32 Commits

Author SHA1 Message Date
undef-i
44bc27a366 chore: update loco_type_info.csv 2025-09-03 19:04:10 +08:00
undef-i
0d6c11e2bd chore 2025-08-29 13:27:25 +08:00
undef-i
92445e681f chore 2025-08-28 21:15:13 +08:00
Nedifinita
78cc909ec8 feat: add database import and export functions 2025-08-28 19:48:12 +08:00
Nedifinita
077e0e4266 refactor: clean up dead code 2025-08-28 14:14:45 +08:00
Nedifinita
0bf7033c6c feat: update locomotive type support and optimized position display format 2025-08-19 19:11:52 +08:00
Nedifinita
0f98b6bcf7 refactor: optimize train record merging logic 2025-08-19 18:06:02 +08:00
Nedifinita
8894a73999 fix: optimize the display logic of history item spacing 2025-08-19 17:43:21 +08:00
Nedifinita
cd4b58e16b fix: correct the default value handling issue when the train display is empty 2025-08-19 17:03:09 +08:00
Nedifinita
39effddfc1 feat: add LocoTypeUtil 2025-08-19 16:35:47 +08:00
undef-i
c4b06f3b3c fix: typo 2025-08-17 14:24:54 +08:00
Nedifinita
eb33fa7feb chore: update project name and .gitignore file 2025-08-08 19:20:26 +08:00
undef-i
65bf7b52c6 fix: correct the error in train_number_info.csv 2025-08-05 20:43:16 +08:00
undef-i
4278de2a8d fix: correct incorrect rendering and status of map marker points 2025-08-03 18:40:14 +08:00
Nedifinita
59e9987d7f feat: add train type recognition and restructure settings storage 2025-08-01 20:07:35 +08:00
Nedifinita
4e97dcafd7 fix: hide duplicate info when train and loco both match 2025-08-01 17:51:24 +08:00
Nedifinita
4cad3679a9 fix: improve TRAIN_OR_LOCO merge display and settings scroll 2025-08-01 17:44:26 +08:00
Nedifinita
e6e7831b96 refactor: migrate data storage from SharedPreferences to Room database 2025-08-01 17:36:04 +08:00
Nedifinita
39bb8cb440 fix: optimize the logic for saving scroll position 2025-08-01 17:35:34 +08:00
Nedifinita
be8dc6bc72 feat: add custom train information notification layout 2025-07-26 17:31:09 +08:00
Nedifinita
cd3128c24b feat: add option for automatically connecting to Bluetooth devices 2025-07-26 17:08:08 +08:00
Nedifinita
e1773370d6 fix: simplify device name matching logic 2025-07-26 01:00:24 +08:00
Nedifinita
c8ab5f7ff8 feat: add LBJ message notification 2025-07-26 00:40:45 +08:00
Nedifinita
e1d02a8a55 feat: add background keep-alive service and related setting functions 2025-07-26 00:19:56 +08:00
Nedifinita
aaf414d384 refactor: optimize record management and UI interaction logic
- Move the loading and saving operations of TrainRecordManager to the IO goroutine for execution
- Optimize the data structure of recentRecords in MainActivity to be a mutableStateList
- Improve the interaction effect and device connection status display of ConnectionDialog
- Delete the MergedHistoryScreen file that is no longer in use
- Increase the number of threads for map tile downloads and file system operations
2025-07-25 23:40:14 +08:00
Nedifinita
3edc8632be feat: add animation effects and visual feedback 2025-07-22 23:18:50 +08:00
Nedifinita
799410eeb2 feat: add BLE disconnection cleanup and enhance record management 2025-07-22 17:29:15 +08:00
Nedifinita
d64138cea5 feat: add record merging functionality and optimize settings page 2025-07-19 21:07:11 +08:00
Nedifinita
a1a9a479f9 feat: enhance MainActivity UI with edge-to-edge support and improved TopAppBar layout in HistoryScreen 2025-07-19 19:10:58 +08:00
Nedifinita
9389ef6e6a refactor: improve layout and formatting of HistoryScreen UI components 2025-07-19 18:32:15 +08:00
Nedifinita
a60b8c58ff feat: add timestamp logging for received messages, optimize page details 2025-07-18 23:53:55 +08:00
Nedifinita
936b960d6a feat: modernize bluetooth apis 2025-07-18 18:54:36 +08:00
63 changed files with 5167 additions and 2447 deletions

150
.gitignore vendored
View File

@@ -13,9 +13,155 @@ captures
.externalNativeBuild
.cxx
local.properties
local.properties
*.ps1
.*.bat
*.jks
*.keystore
*.base64
*.base64
docs
linux
windows
flutter/ephemeral/
*.suo
*.user
*.userosscache
*.sln.docstates
x64/
x86/
*.[Cc]ache
!*.[Cc]ache/
.gradle/
build/
local.properties
*.log
captures/
.externalNativeBuild/
.cxx/
*.apk
output.json
*.iml
.idea/
misc.xml
deploymentTargetDropDown.xml
render.experimental.xml
*.jks
*.keystore
google-services.json
*.hprof
gen-external-apklibs
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.fvm/flutter_sdk
.packages
.pub-cache/
.pub/
coverage/
lib/generated_plugin_registrant.dart
**/android/**/gradle-wrapper.jar
**/android/.gradle
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
**/android/key.properties
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
**/ios/**/*.mode1v3
**/ios/**/*.mode2v3
**/ios/**/*.moved-aside
**/ios/**/*.pbxuser
**/ios/**/*.perspectivev3
**/ios/**/*sync/
**/ios/**/.sconsign.dblite
**/ios/**/.tags*
**/ios/**/.vagrant/
**/ios/**/DerivedData/
**/ios/**/Icon?
**/ios/**/Pods/
**/ios/**/.symlinks/
**/ios/**/profile
**/ios/**/xcuserdata
**/ios/.generated/
**/ios/Flutter/.last_build_id
**/ios/Flutter/App.framework
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Flutter.podspec
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/app.flx
**/ios/Flutter/app.zip
**/ios/Flutter/flutter_assets/
**/ios/Flutter/flutter_export_environment.sh
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
!**/ios/**/default.mode1v3
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
*.ap_
*.aab
*.dex
*.class
bin/
gen/
out/
.gradle
.signing/
proguard/
/*/build/
/*/local.properties
/*/out
/*/*/build
/*/*/production
.navigation/
*.ipr
*~
*.swp
.externalNativeBuild
obj/
*.iws
/out/
.idea/caches/
.idea/libraries/
.idea/shelf/
.idea/workspace.xml
.idea/tasks.xml
.idea/.name
.idea/compiler.xml
.idea/copyright/profiles_settings.xml
.idea/encodings.xml
.idea/misc.xml
.idea/modules.xml
.idea/scopes/scope_settings.xml
.idea/dictionaries
.idea/vcs.xml
.idea/jsLibraryMappings.xml
.idea/datasources.xml
.idea/dataSources.ids
.idea/sqlDataSources.xml
.idea/dynamic.xml
.idea/uiDesigner.xml
.idea/assetWizardSettings.xml
.idea/gradle.xml
.idea/jarRepositories.xml
.idea/navEditor.xml
.classpath
.project
.cproject
.settings/
.mtj.tmp/
*.war
*.ear
hs_err_pid*
.idea_modules/
atlassian-ide-plugin.xml
.idea/mongoSettings.xml
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
!/gradle/wrapper/gradle-wrapper.jar
macos/Flutter/ephemeral/flutter_export_environment.sh
macos/Flutter/ephemeral/Flutter-Generated.xcconfig
*.py

2
.idea/.name generated
View File

@@ -1 +1 @@
LBJ Receiver
LBJ_Console

View File

@@ -2,6 +2,12 @@
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="Unnamed">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
<SelectionState runConfigName="lbjconsole_android">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>

View File

@@ -1,15 +1,20 @@
# LBJ Console
LBJ Console is an Android app designed to receive and display LBJ messages via BLE from the [SX1276_Receive_LBJ](https://github.com/undef-i/SX1276_Receive_LBJ) device.
LBJ Console 是一款应用程序,用于通过 BLE 从 [SX1276_Receive_LBJ](https://github.com/undef-i/SX1276_Receive_LBJ) 设备接收并显示列车预警消息,功能包括:
## Roadmap
- Tab state persistence
- Record filtering (train number, time range)
- Record management page optimization
- Optional train merge by locomotive/number
- Offline data storage
- Add record timestamps
- 接收列车预警消息,支持可选的手机推送通知。
- 在地图上显示预警消息的 GPS 信息。
- 基于内置数据文件显示机车配属,机车类型和车次类型。
# License
主分支目前只适配了 Android 。如需在其它平台上面使用,请参考 [flutter](https://github.com/undef-i/LBJ_Console/tree/flutter) 分支自行编译。
## 数据文件
This project is licensed under the GNU General Public License v3.0 (GPLv3). This license ensures that the software remains free and open source, requiring that any modifications or derivative works must also be released under the same license terms.
LBJ Console 依赖以下数据文件,位于 `app/src/main/assets/` 目录,用于支持机车配属和车次信息的展示:
- `loco_info.csv`:包含机车配属信息,格式为 `机车型号,机车编号起始值,机车编号结束值,所属铁路局及机务段,备注`
- `loco_type_info.csv`:包含机车类型编码信息,格式为 `机车类型编码,机车类型`
- `train_info.csv`:包含车次类型信息,格式为 `正则表达式,车次类型`
# 许可证
该项目采用 GNU 通用公共许可证 v3.0GPLv3授权。该许可证确保软件保持免费和开源要求任何修改或衍生作品也必须在相同许可证条款下发布。

View File

@@ -2,6 +2,7 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.ksp)
}
android {
@@ -12,8 +13,8 @@ android {
applicationId = "org.noxylva.lbjconsole"
minSdk = 29
targetSdk = 35
versionCode = 1
versionName = "0.0.1"
versionCode = 15
versionName = "0.1.5"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -59,6 +60,7 @@ android {
}
lint {
disable += "NullSafeMutableLiveData"
warning += "MissingPermission"
}
}
@@ -81,8 +83,14 @@ dependencies {
debugImplementation(libs.androidx.ui.test.manifest)
implementation("org.json:json:20231013")
implementation("androidx.compose.material:material-icons-extended:1.5.4")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("org.osmdroid:osmdroid-android:6.1.16")
implementation("org.osmdroid:osmdroid-mapsforge:6.1.16")
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.startup.runtime)
implementation("com.google.code.gson:gson:2.10.1")
}

View File

@@ -11,6 +11,13 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
@@ -22,14 +29,14 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.LBJReceiver"
android:theme="@style/Theme.LBJConsole"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.LBJReceiver">
android:theme="@style/Theme.LBJConsole">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -37,6 +44,18 @@
</intent-filter>
</activity>
<activity
android:name=".FilePickerActivity"
android:exported="false"
android:theme="@style/Theme.LBJConsole"
android:label="数据管理" />
<service
android:name=".BackgroundService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="dataSync" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"

View File

@@ -0,0 +1,143 @@
001,解放
003,前进
005,建设
006,KD7
055,蓝箭控车
081,东风21
101,东风
102,东风2
103,东风3
104,东风4
105,东风4客
106,东风4C
107,东风5
108,东风5宽
109,东风6
110,东风7
111,东风8
112,东风9
113,东风10
114,东方红1
115,东方红2
116,东方红3
117,东方红5
118,北京
119,北京宽
120,ND2
121,ND3
122,ND4
123,ND5
124,NY5
125,NY6
126,NY7
127,轻油
128,东方红21
129,东风7B
130,东风5S
131,东风7C
132,东风7S
133,工矿1
134,工矿1F
135,东风4E
136,东风7D
137,工矿1A
138,东风11
139,天安
140,东风10F
141,东风4D
142,东风8B
143,东风12
144,东风7E
145,NYJ1
146,NZJ1
147,NZJ2
148,东风4DJ
149,新曙光
150,神州
151,NJ2
152,东风7G
153,NDJ3
157,FXN3D
158,东风11G
160,HXN3
161,HXN5
162,HXN3B
163,HXN5B
167,FXN3B
169,FXN3C
170,FXN5C
171,FXN3-J
201,8G
202,8K
203,6G
204,6K
205,韶山1
206,韶山3
207,韶山4
208,韶山5
209,韶山6
210,韶山3B
211,韶山7
212,韶山8
213,韶山7B
214,韶山7C
215,韶山6B
216,韶山9
217,韶山7D
218,DJ熊猫
219,DJ1
220,DJ2
221,DJF
222,蓝箭动车
223,先锋号
224,韶山7E
225,韶山4G
226,韶山3C
228,天梭
229,DJ4和谐
230,KTT
231,HXD1
232,HXD2
233,HXD3
234,HXD1B
235,HXD2B
236,HXD3B
237,HXD1C
238,HXD2C
239,HXD3C
240,HXD1D
241,HXD2D
242,HXD3D
243,FXD1B
244,FXD2B
245,FXD1
246,FXD3
247,FXD1-J
248,FXD3-J
249,KZ25TA
251,KZ25TB
252,HXD1D-J
254,FXD1H
300,雪域神州
301,CRH1
302,CRH2
303,CRH3
305,CRH5
306,CRH380A
307,CRH380B
308,CRH380C
309,CRH380D
310,CRH6A
311,CR400AF
312,CR400BF
313,CR300AF
314,CR300BF
315,CRH2E
316,CRH6F
330,CJ1
331,CJ2
332,CJ3
333,CJ4
334,CJ5
335,CJ6
400,GCD-1000J
1 001 解放
2 003 前进
3 005 建设
4 006 KD7
5 055 蓝箭控车
6 081 东风21
7 101 东风
8 102 东风2
9 103 东风3
10 104 东风4
11 105 东风4客
12 106 东风4C
13 107 东风5
14 108 东风5宽
15 109 东风6
16 110 东风7
17 111 东风8
18 112 东风9
19 113 东风10
20 114 东方红1
21 115 东方红2
22 116 东方红3
23 117 东方红5
24 118 北京
25 119 北京宽
26 120 ND2
27 121 ND3
28 122 ND4
29 123 ND5
30 124 NY5
31 125 NY6
32 126 NY7
33 127 轻油
34 128 东方红21
35 129 东风7B
36 130 东风5S
37 131 东风7C
38 132 东风7S
39 133 工矿1
40 134 工矿1F
41 135 东风4E
42 136 东风7D
43 137 工矿1A
44 138 东风11
45 139 天安
46 140 东风10F
47 141 东风4D
48 142 东风8B
49 143 东风12
50 144 东风7E
51 145 NYJ1
52 146 NZJ1
53 147 NZJ2
54 148 东风4DJ
55 149 新曙光
56 150 神州
57 151 NJ2
58 152 东风7G
59 153 NDJ3
60 157 FXN3D
61 158 东风11G
62 160 HXN3
63 161 HXN5
64 162 HXN3B
65 163 HXN5B
66 167 FXN3B
67 169 FXN3C
68 170 FXN5C
69 171 FXN3-J
70 201 8G
71 202 8K
72 203 6G
73 204 6K
74 205 韶山1
75 206 韶山3
76 207 韶山4
77 208 韶山5
78 209 韶山6
79 210 韶山3B
80 211 韶山7
81 212 韶山8
82 213 韶山7B
83 214 韶山7C
84 215 韶山6B
85 216 韶山9
86 217 韶山7D
87 218 DJ熊猫
88 219 DJ1
89 220 DJ2
90 221 DJF
91 222 蓝箭动车
92 223 先锋号
93 224 韶山7E
94 225 韶山4G
95 226 韶山3C
96 228 天梭
97 229 DJ4和谐
98 230 KTT
99 231 HXD1
100 232 HXD2
101 233 HXD3
102 234 HXD1B
103 235 HXD2B
104 236 HXD3B
105 237 HXD1C
106 238 HXD2C
107 239 HXD3C
108 240 HXD1D
109 241 HXD2D
110 242 HXD3D
111 243 FXD1B
112 244 FXD2B
113 245 FXD1
114 246 FXD3
115 247 FXD1-J
116 248 FXD3-J
117 249 KZ25TA
118 251 KZ25TB
119 252 HXD1D-J
120 254 FXD1H
121 300 雪域神州
122 301 CRH1
123 302 CRH2
124 303 CRH3
125 305 CRH5
126 306 CRH380A
127 307 CRH380B
128 308 CRH380C
129 309 CRH380D
130 310 CRH6A
131 311 CR400AF
132 312 CR400BF
133 313 CR300AF
134 314 CR300BF
135 315 CRH2E
136 316 CRH6F
137 330 CJ1
138 331 CJ2
139 332 CJ3
140 333 CJ4
141 334 CJ5
142 335 CJ6
143 400 GCD-1000J

View File

@@ -0,0 +1,82 @@
"^[Gg](4000|[1-3]\d{3}|[1-9]\d{0,2})$","直通图定高速动车组"
"^[Gg](400[1-9]|40[1-9]\d|4[1-8]\d{2}|49[0-8]\d|499[0-8])$","直通临客高速动车组"
"^[Gg](9000|[6-8]\d{3}|500[1-9]|50[1-9]\d|5[1-9]\d{2})$","管内图定高速动车组"
"^[Gg](900[1-9]|90[1-9]\d|9[1-8]\d{2}|99[0-8]\d|999[0-8])$","管内临客高速动车组"
"^[Cc]([1-8]\d{3}|9000)$","图定城际动车组"
"^[Cc](900[1-9]|90[1-9]\d|9[1-8]\d{2}|99[0-8]\d|999[0-8])$","临客城际动车组"
"^[Cc][1-9]\d{2}$","动力集中城际动车组"
"^[IDid](4000|[1-3]\d{3}|[1-9]\d{0,2})$","直通图定动车组"
"^[IDid](400[1-9]|40[1-9]\d|4[1-8]\d{2}|49[0-8]\d|499[0-8])$","直通临客动车组"
"^[IDid](9000|[6-8]\d{3}|500[1-9]|50[1-9]\d|5[1-9]\d{2})$","管内图定动车组"
"^[IDid](900[1-9]|90[1-9]\d|9[1-8]\d{2}|99[0-8]\d|999[0-8])$","管内临客动车组"
"^[IDid](8([0-8]\d|9[0-8])|7(0[1-9]|[1-9]\d))$","动力集中动车组"
"^[IDid](300|[12]\d{2}|[1-9]\d?)$","跨局动力集中动车组"
"^[PZpz](4000|[1-3]\d{3}|[1-9]\d{0,2})$","直通图定直达特快旅客列车"
"^[PZpz](400[1-9]|40[1-9]\d|4[1-8]\d{2}|49[0-8]\d|499[0-8])$","直通临客直达特快旅客列车"
"^[PZpz](9000|[6-8]\d{3}|500[1-9]|50[1-9]\d|5[1-9]\d{2})$","管内图定直达特快旅客列车"
"^[PZpz](900[1-9]|90[1-9]\d|9[1-8]\d{2}|99[0-8]\d|999[0-8])$","管内临客直达特快旅客列车"
"^[QTqt](3000|[12]\d{3}|[1-9]\d{0,2})$","直通图定特快旅客列车"
"^[QTqt](300[1-9]|30[1-9]\d|3[1-8]\d{2}|39[0-8]\d|399[0-8])$","直通临客特快旅客列车"
"^[QTqt](4(00[1-9]|0[1-9]\d|[1-8]\d{2}|9[0-8]\d|99[0-8]))$","管内临客特快旅客列车"
"^[QTqt]([5-8]\d{3}|9([0-8]\d{2}|9[0-8]\d|99[0-8])|500[1-9]|50[1-9]\d|5[1-9]\d{2})$","管内图定特快旅客列车"
"^[WKwk](4000|[1-3]\d{3}|[1-9]\d{0,2})$","直通图定快速旅客列车"
"^[WKwk](400[1-9]|40[1-9]\d|4[1-8]\d{2}|49[0-8]\d|499[0-8])$","直通临客快速旅客列车"
"^[WKwk](6([0-8]\d{2}|9[0-8]\d|99[0-8])|500[1-9]|50[1-9]\d|5[1-9]\d{2})$","管内临客快速旅客列车"
"^[WKwk](8\d{3}|9([0-8]\d{2}|9[0-8]\d|99[0-8])|7(00[1-9]|0[1-9]\d|[1-9]\d{2}))$","管内图定快速旅客列车"
"^[Vv1](00[1-9]|0[1-9]\d|[1-9]\d{2})$","跨三局及以上图定普通旅客快车"
"^[Bb2](00[1-9]|0[1-9]\d|[1-9]\d{2})$","跨两局图定普通旅客快车"
"^3(00[1-9]|0[1-9]\d|[1-9]\d{2})$","跨局临时普通旅客快车"
"^[Uu4](00[1-9]|0[1-9]\d|[1-9]\d{2})$","管内图定普通旅客快车"
"^[Xx5](000|1[9][9]|200|3[9][9]|400)$","管内图定普通旅客快车"
"^6(19[0-8]|1[0-8]\d|0[1-9]\d|00[1-9])$","直通普通旅客慢车"
"^(6(20[1-9]|2[1-9]\d|[3-9]\d{2})|7([0-4]\d{2}|5([0-8]\d|9[0-8])))$","管内普通旅客慢车"
"^(8([0-8]\d{2}|9[0-8]\d|99[0-8])|7(60[1-9]|6[1-9]\d|[7-9]\d{2}))$","通勤列车"
"^[Yy](500|[1-4]\d{2}|[1-9]\d?)$","跨局旅游列车"
"^[Yy](50[1-9]|5[1-9]\d|[6-9]\d{2})$","管内旅游列车"
"^[Ss][1-9]\d{0,3}$","市郊旅客列车"
"^[Ll](6([0-8]\d{2}|9[0-8]\d|99[0-8])|[1-5]\d{3}|[1-9]\d{0,2})$","直通临时旅客列车"
"^[Ll]([7-9]\d{3})$","管内临时旅客列车"
"^[Xx](19[0-8]|1[0-8]\d|[1-9]\d?)$","特快货物班列"
"^[Xx](39[0-8]|3[0-8]\d|2[1-9]\d|20[1-9])$","快速货物班列"
"^[Xx]2(40[1-9]|4[1-9]\d|[5-9]\d{2})$","直通货物快运列车"
"^[Xx]([4-9]\d{2}|4[1-9]\d|40[1-9])$","管内货物快运列车"
"^[Xx]8\d{3}$","中欧中亚集装箱班列"
"^[Xx]9([0-4]\d{2}|500)$","中亚集装箱班列"
"^[Xx]9(50[1-9]|5[1-9]\d|[6-9]\d{2})$","水铁联运班列"
"^[Xx][1-4]\d{4}$","加挂零散快运车辆货物列车"
"^1(000[1-9]|00[1-9]\d|0[1-9]\d{2}|[1-9]\d{3})$","技术直达列车"
"^2\d{4}$","直通货物列车"
"^3\d{4}$","区段摘挂列车"
"^4([0-3]\d{3}|4([0-8]\d{2}|9[0-8]\d|99[0-8]))$","摘挂列车"
"^4(500[1-9]|50[1-9]\d|5[1-9]\d{2}|[6-9]\d{3})$","小运转列车"
"^6\d{4}$","自备列车"
"^70\d{3}$","超限货物列车"
"^7([1-6]\d{3}|7([0-8]\d{2}|9[0-8]\d|99[0-8]))$","重载货物列车"
"^78\d{3}$","保温列车"
"^8(0\d{3}|1([0-8]\d{2}|9[0-8]\d|99[0-8]))$","普快货物班列"
"^8(200[1-9]|20[1-9]\d|2[1-9]\d{2}|[34]\d{3})$","煤炭直达列车"
"^85\d{3}$","石油直达列车"
"^86\d{3}$","始发直达列车"
"^87\d{3}$","空车直达列车"
"^(90\d{3}|91([0-8]\d{2}|9[0-8]\d|99[0-8]))$","军用列车"
"^50\d{3}$","客车单机"
"^51\d{3}$","货车单机"
"^52\d{3}$","小运转单机"
"^5(3\d{3}|4([0-8]\d{2}|9[0-8]\d|99[0-8]))$","补机列车"
"^55(300|[0-2]\d{2})$","普通客货试运转列车"
"^55(500|30[1-9]|3[1-9]\d|4\d{2})$","高速动车组试运转列车"
"^55(50[1-9]|5[1-9]\d|[6-9]\d{2})$","普通动车组试运转列车"
"^56\d{3}$","轻油动车与轨道车"
"^57\d{3}$","路用列车"
"^58(10[1-9]|1[1-9]\d|[2-8]\d{2}|9([0-8]\d|9[0-8]))$","救援列车"
"^DJ(400|[1-3]\d{2}|[1-9]\d?)$","动车组检测列车300直通"
"^DJ([4-9]\d{2}|40[1-9]|4[1-9]\d)$","动车组检测列车300管内"
"^DJ1(400|[0-3]\d{2})$","动车组检测列车250直通"
"^DJ1(40[1-9]|4[1-9]\d|[5-9]\d{2})$","动车组检测列车250管内"
"^DJ[56]\d{3}$","直通动车组确认列车"
"^DJ[78]\d{3}$","管内动车组确认列车"
"^[Ff][GDCZTKgdcztk]?\d{1,4}$","因故折返旅客列车"
"^0[GDCZTKgdcztk]\d{1,4}$","回送图定客车底"
"^00(100|[1-9]\d?)$","有火回送动车组车底"
"^00(10[1-9]|1[1-9]\d|2([0-8]\d|9[0-8]))$","无火回送动车组车底"
"^00(30[1-9]|3[1-9]\d|4([0-8]\d|9[0-8]))$","无火回送普速客车底"
1 ^[Gg](4000|[1-3]\d{3}|[1-9]\d{0,2})$ 直通图定高速动车组
2 ^[Gg](400[1-9]|40[1-9]\d|4[1-8]\d{2}|49[0-8]\d|499[0-8])$ 直通临客高速动车组
3 ^[Gg](9000|[6-8]\d{3}|500[1-9]|50[1-9]\d|5[1-9]\d{2})$ 管内图定高速动车组
4 ^[Gg](900[1-9]|90[1-9]\d|9[1-8]\d{2}|99[0-8]\d|999[0-8])$ 管内临客高速动车组
5 ^[Cc]([1-8]\d{3}|9000)$ 图定城际动车组
6 ^[Cc](900[1-9]|90[1-9]\d|9[1-8]\d{2}|99[0-8]\d|999[0-8])$ 临客城际动车组
7 ^[Cc][1-9]\d{2}$ 动力集中城际动车组
8 ^[IDid](4000|[1-3]\d{3}|[1-9]\d{0,2})$ 直通图定动车组
9 ^[IDid](400[1-9]|40[1-9]\d|4[1-8]\d{2}|49[0-8]\d|499[0-8])$ 直通临客动车组
10 ^[IDid](9000|[6-8]\d{3}|500[1-9]|50[1-9]\d|5[1-9]\d{2})$ 管内图定动车组
11 ^[IDid](900[1-9]|90[1-9]\d|9[1-8]\d{2}|99[0-8]\d|999[0-8])$ 管内临客动车组
12 ^[IDid](8([0-8]\d|9[0-8])|7(0[1-9]|[1-9]\d))$ 动力集中动车组
13 ^[IDid](300|[12]\d{2}|[1-9]\d?)$ 跨局动力集中动车组
14 ^[PZpz](4000|[1-3]\d{3}|[1-9]\d{0,2})$ 直通图定直达特快旅客列车
15 ^[PZpz](400[1-9]|40[1-9]\d|4[1-8]\d{2}|49[0-8]\d|499[0-8])$ 直通临客直达特快旅客列车
16 ^[PZpz](9000|[6-8]\d{3}|500[1-9]|50[1-9]\d|5[1-9]\d{2})$ 管内图定直达特快旅客列车
17 ^[PZpz](900[1-9]|90[1-9]\d|9[1-8]\d{2}|99[0-8]\d|999[0-8])$ 管内临客直达特快旅客列车
18 ^[QTqt](3000|[12]\d{3}|[1-9]\d{0,2})$ 直通图定特快旅客列车
19 ^[QTqt](300[1-9]|30[1-9]\d|3[1-8]\d{2}|39[0-8]\d|399[0-8])$ 直通临客特快旅客列车
20 ^[QTqt](4(00[1-9]|0[1-9]\d|[1-8]\d{2}|9[0-8]\d|99[0-8]))$ 管内临客特快旅客列车
21 ^[QTqt]([5-8]\d{3}|9([0-8]\d{2}|9[0-8]\d|99[0-8])|500[1-9]|50[1-9]\d|5[1-9]\d{2})$ 管内图定特快旅客列车
22 ^[WKwk](4000|[1-3]\d{3}|[1-9]\d{0,2})$ 直通图定快速旅客列车
23 ^[WKwk](400[1-9]|40[1-9]\d|4[1-8]\d{2}|49[0-8]\d|499[0-8])$ 直通临客快速旅客列车
24 ^[WKwk](6([0-8]\d{2}|9[0-8]\d|99[0-8])|500[1-9]|50[1-9]\d|5[1-9]\d{2})$ 管内临客快速旅客列车
25 ^[WKwk](8\d{3}|9([0-8]\d{2}|9[0-8]\d|99[0-8])|7(00[1-9]|0[1-9]\d|[1-9]\d{2}))$ 管内图定快速旅客列车
26 ^[Vv1](00[1-9]|0[1-9]\d|[1-9]\d{2})$ 跨三局及以上图定普通旅客快车
27 ^[Bb2](00[1-9]|0[1-9]\d|[1-9]\d{2})$ 跨两局图定普通旅客快车
28 ^3(00[1-9]|0[1-9]\d|[1-9]\d{2})$ 跨局临时普通旅客快车
29 ^[Uu4](00[1-9]|0[1-9]\d|[1-9]\d{2})$ 管内图定普通旅客快车
30 ^[Xx5](000|1[9][9]|200|3[9][9]|400)$ 管内图定普通旅客快车
31 ^6(19[0-8]|1[0-8]\d|0[1-9]\d|00[1-9])$ 直通普通旅客慢车
32 ^(6(20[1-9]|2[1-9]\d|[3-9]\d{2})|7([0-4]\d{2}|5([0-8]\d|9[0-8])))$ 管内普通旅客慢车
33 ^(8([0-8]\d{2}|9[0-8]\d|99[0-8])|7(60[1-9]|6[1-9]\d|[7-9]\d{2}))$ 通勤列车
34 ^[Yy](500|[1-4]\d{2}|[1-9]\d?)$ 跨局旅游列车
35 ^[Yy](50[1-9]|5[1-9]\d|[6-9]\d{2})$ 管内旅游列车
36 ^[Ss][1-9]\d{0,3}$ 市郊旅客列车
37 ^[Ll](6([0-8]\d{2}|9[0-8]\d|99[0-8])|[1-5]\d{3}|[1-9]\d{0,2})$ 直通临时旅客列车
38 ^[Ll]([7-9]\d{3})$ 管内临时旅客列车
39 ^[Xx](19[0-8]|1[0-8]\d|[1-9]\d?)$ 特快货物班列
40 ^[Xx](39[0-8]|3[0-8]\d|2[1-9]\d|20[1-9])$ 快速货物班列
41 ^[Xx]2(40[1-9]|4[1-9]\d|[5-9]\d{2})$ 直通货物快运列车
42 ^[Xx]([4-9]\d{2}|4[1-9]\d|40[1-9])$ 管内货物快运列车
43 ^[Xx]8\d{3}$ 中欧中亚集装箱班列
44 ^[Xx]9([0-4]\d{2}|500)$ 中亚集装箱班列
45 ^[Xx]9(50[1-9]|5[1-9]\d|[6-9]\d{2})$ 水铁联运班列
46 ^[Xx][1-4]\d{4}$ 加挂零散快运车辆货物列车
47 ^1(000[1-9]|00[1-9]\d|0[1-9]\d{2}|[1-9]\d{3})$ 技术直达列车
48 ^2\d{4}$ 直通货物列车
49 ^3\d{4}$ 区段摘挂列车
50 ^4([0-3]\d{3}|4([0-8]\d{2}|9[0-8]\d|99[0-8]))$ 摘挂列车
51 ^4(500[1-9]|50[1-9]\d|5[1-9]\d{2}|[6-9]\d{3})$ 小运转列车
52 ^6\d{4}$ 自备列车
53 ^70\d{3}$ 超限货物列车
54 ^7([1-6]\d{3}|7([0-8]\d{2}|9[0-8]\d|99[0-8]))$ 重载货物列车
55 ^78\d{3}$ 保温列车
56 ^8(0\d{3}|1([0-8]\d{2}|9[0-8]\d|99[0-8]))$ 普快货物班列
57 ^8(200[1-9]|20[1-9]\d|2[1-9]\d{2}|[34]\d{3})$ 煤炭直达列车
58 ^85\d{3}$ 石油直达列车
59 ^86\d{3}$ 始发直达列车
60 ^87\d{3}$ 空车直达列车
61 ^(90\d{3}|91([0-8]\d{2}|9[0-8]\d|99[0-8]))$ 军用列车
62 ^50\d{3}$ 客车单机
63 ^51\d{3}$ 货车单机
64 ^52\d{3}$ 小运转单机
65 ^5(3\d{3}|4([0-8]\d{2}|9[0-8]\d|99[0-8]))$ 补机列车
66 ^55(300|[0-2]\d{2})$ 普通客货试运转列车
67 ^55(500|30[1-9]|3[1-9]\d|4\d{2})$ 高速动车组试运转列车
68 ^55(50[1-9]|5[1-9]\d|[6-9]\d{2})$ 普通动车组试运转列车
69 ^56\d{3}$ 轻油动车与轨道车
70 ^57\d{3}$ 路用列车
71 ^58(10[1-9]|1[1-9]\d|[2-8]\d{2}|9([0-8]\d|9[0-8]))$ 救援列车
72 ^DJ(400|[1-3]\d{2}|[1-9]\d?)$ 动车组检测列车300直通
73 ^DJ([4-9]\d{2}|40[1-9]|4[1-9]\d)$ 动车组检测列车300管内
74 ^DJ1(400|[0-3]\d{2})$ 动车组检测列车250直通
75 ^DJ1(40[1-9]|4[1-9]\d|[5-9]\d{2})$ 动车组检测列车250管内
76 ^DJ[56]\d{3}$ 直通动车组确认列车
77 ^DJ[78]\d{3}$ 管内动车组确认列车
78 ^[Ff][GDCZTKgdcztk]?\d{1,4}$ 因故折返旅客列车
79 ^0[GDCZTKgdcztk]\d{1,4}$ 回送图定客车底
80 ^00(100|[1-9]\d?)$ 有火回送动车组车底
81 ^00(10[1-9]|1[1-9]\d|2([0-8]\d|9[0-8]))$ 无火回送动车组车底
82 ^00(30[1-9]|3[1-9]\d|4([0-8]\d|9[0-8]))$ 无火回送普速客车底

View File

@@ -18,9 +18,7 @@ import java.util.*
class BLEClient(private val context: Context) : BluetoothGattCallback() {
companion object {
const val TAG = "LBJ_BT"
const val SCAN_PERIOD = 10000L
const val TAG = "LBJ_BT"
val SERVICE_UUID = UUID.fromString("0000ffe0-0000-1000-8000-00805f9b34fb")
val CHAR_UUID = UUID.fromString("0000ffe1-0000-1000-8000-00805f9b34fb")
@@ -42,20 +40,69 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
private var targetDeviceName: String? = null
private var bluetoothLeScanner: BluetoothLeScanner? = null
private var continuousScanning = false
private var autoReconnect = true
private var lastKnownDeviceAddress: String? = null
private var connectionAttempts = 0
private var isReconnecting = false
private var highFrequencyReconnect = true
private var reconnectHandler = Handler(Looper.getMainLooper())
private var reconnectRunnable: Runnable? = null
private var connectionLostCallback: (() -> Unit)? = null
private var connectionSuccessCallback: ((String) -> Unit)? = null
private var specifiedDeviceAddress: String? = null
private var targetDeviceAddress: String? = null
private var isDialogOpen = false
private var isManualDisconnect = false
private var isAutoConnectBlocked = false
private val leScanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
val device = result.device
val deviceName = device.name
if (targetDeviceName != null) {
if (deviceName == null || !deviceName.equals(targetDeviceName, ignoreCase = true)) {
return
val shouldShowDevice = when {
targetDeviceName != null -> {
deviceName != null && deviceName.equals(targetDeviceName, ignoreCase = true)
}
else -> {
true
}
}
if (shouldShowDevice) {
Log.d(TAG, "Showing filtered device: $deviceName")
scanCallback?.invoke(device)
}
if (!isConnected && !isReconnecting && !isDialogOpen && !isAutoConnectBlocked) {
val deviceAddress = device.address
val isSpecifiedDevice = specifiedDeviceAddress == deviceAddress
val isTargetDevice = targetDeviceName != null && deviceName != null && deviceName.equals(targetDeviceName, ignoreCase = true)
val isKnownDevice = lastKnownDeviceAddress == deviceAddress
val isSpecificTargetAddress = targetDeviceAddress == deviceAddress
if (isSpecificTargetAddress || isSpecifiedDevice || (specifiedDeviceAddress == null && isTargetDevice) || (specifiedDeviceAddress == null && isKnownDevice)) {
val priority = when {
isSpecificTargetAddress -> "specific target address"
isSpecifiedDevice -> "specified device"
isTargetDevice -> "target device name"
else -> "known device"
}
Log.i(TAG, "Found device ($priority): $deviceName, auto-connecting")
lastKnownDeviceAddress = deviceAddress
connectImmediately(deviceAddress)
}
}
scanCallback?.invoke(device)
}
override fun onScanFailed(errorCode: Int) {
Log.e(TAG, "BLE scan failed code=$errorCode")
if (continuousScanning) {
handler.post {
restartScan()
}
}
}
}
@@ -90,12 +137,13 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
try {
scanCallback = callback
this.targetDeviceName = targetDeviceName
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() ?: run {
val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
val bluetoothAdapter = bluetoothManager.adapter ?: run {
Log.e(TAG, "Bluetooth adapter unavailable")
return
}
if (!bluetoothAdapter.isEnabled) {
if (bluetoothAdapter.isEnabled != true) {
Log.e(TAG, "Bluetooth adapter disabled")
return
}
@@ -106,12 +154,9 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
return
}
handler.postDelayed({
stopScan()
}, SCAN_PERIOD)
isScanning = true
Log.d(TAG, "Starting BLE scan target=${targetDeviceName ?: "Any"}")
continuousScanning = true
Log.d(TAG, "Starting continuous BLE scan target=${targetDeviceName ?: "Any"}")
bluetoothLeScanner?.startScan(leScanCallback)
} catch (e: SecurityException) {
Log.e(TAG, "Scan security error: ${e.message}")
@@ -126,6 +171,40 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
if (isScanning) {
bluetoothLeScanner?.stopScan(leScanCallback)
isScanning = false
continuousScanning = false
Log.d(TAG, "Stopped BLE scan")
}
}
@SuppressLint("MissingPermission")
private fun restartScan() {
if (!continuousScanning) return
try {
bluetoothLeScanner?.stopScan(leScanCallback)
bluetoothLeScanner?.startScan(leScanCallback)
isScanning = true
Log.d(TAG, "Restarted BLE scan")
} catch (e: Exception) {
Log.e(TAG, "Failed to restart scan: ${e.message}")
}
}
private fun connectImmediately(address: String) {
if (isReconnecting) return
isReconnecting = true
handler.post {
connect(address) { connected ->
isReconnecting = false
if (connected) {
connectionAttempts = 0
Log.i(TAG, "Successfully connected to $address")
} else {
connectionAttempts++
Log.w(TAG, "Connection attempt $connectionAttempts failed for $address")
}
}
}
}
@@ -150,13 +229,14 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
}
try {
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() ?: run {
val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
val bluetoothAdapter = bluetoothManager.adapter ?: run {
Log.e(TAG, "Bluetooth adapter unavailable")
handler.post { onConnectionStateChange?.invoke(false) }
return false
}
if (!bluetoothAdapter.isEnabled) {
if (bluetoothAdapter.isEnabled != true) {
Log.e(TAG, "Bluetooth adapter is disabled")
handler.post { onConnectionStateChange?.invoke(false) }
return false
@@ -181,17 +261,7 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
bluetoothGatt = device.connectGatt(context, false, this, BluetoothDevice.TRANSPORT_LE)
Log.d(TAG, "Connecting to address=$address")
handler.postDelayed({
if (!isConnected && deviceAddress == address) {
Log.e(TAG, "Connection timeout reconnecting")
bluetoothGatt?.close()
bluetoothGatt =
device.connectGatt(context, false, this, BluetoothDevice.TRANSPORT_LE)
}
}, 10000)
return true
} catch (e: Exception) {
@@ -205,11 +275,97 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
fun isConnected(): Boolean {
return isConnected
}
@SuppressLint("MissingPermission")
fun checkActualConnectionState(): Boolean {
bluetoothGatt?.let { gatt ->
try {
val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
val connectedDevices = bluetoothManager.getConnectedDevices(BluetoothProfile.GATT)
val isActuallyConnected = connectedDevices.any { it.address == deviceAddress }
if (isActuallyConnected && !isConnected) {
Log.d(TAG, "Found existing GATT connection, updating internal state")
isConnected = true
return true
} else if (!isActuallyConnected && isConnected) {
Log.d(TAG, "GATT connection lost, updating internal state")
isConnected = false
return false
}
return isActuallyConnected
} catch (e: Exception) {
Log.e(TAG, "Error checking actual connection state: ${e.message}")
return isConnected
}
}
return isConnected
}
@SuppressLint("MissingPermission")
fun disconnect() {
bluetoothGatt?.disconnect()
Log.d(TAG, "Manual disconnect initiated")
isConnected = false
isManualDisconnect = true
isAutoConnectBlocked = true
stopHighFrequencyReconnect()
stopScan()
bluetoothGatt?.let { gatt ->
try {
gatt.disconnect()
Thread.sleep(100)
gatt.close()
} catch (e: Exception) {
Log.e(TAG, "Disconnect error: ${e.message}")
}
}
bluetoothGatt = null
dataBuffer.clear()
connectionStateCallback = null
Log.d(TAG, "Manual disconnect - auto connect blocked, deviceAddress preserved: $deviceAddress")
}
@SuppressLint("MissingPermission")
fun connectManually(address: String, onConnectionStateChange: ((Boolean) -> Unit)? = null): Boolean {
Log.d(TAG, "Manual connection to device: $address")
stopScan()
stopHighFrequencyReconnect()
isManualDisconnect = false
isAutoConnectBlocked = false
autoReconnect = true
highFrequencyReconnect = true
return connect(address, onConnectionStateChange)
}
@SuppressLint("MissingPermission")
fun closeManually() {
Log.d(TAG, "Manual close - will restore auto reconnect")
isConnected = false
isManualDisconnect = false
isAutoConnectBlocked = false
bluetoothGatt?.let { gatt ->
try {
gatt.disconnect()
gatt.close()
} catch (e: Exception) {
Log.e(TAG, "Close error: ${e.message}")
}
}
bluetoothGatt = null
deviceAddress = null
autoReconnect = true
highFrequencyReconnect = true
Log.d(TAG, "Auto reconnect mechanism restored and GATT cleaned up")
}
@@ -284,30 +440,31 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
if (status != BluetoothGatt.GATT_SUCCESS) {
Log.e(TAG, "Connection error status=$status")
isConnected = false
isReconnecting = false
if (status == 133 || status == 8) {
Log.e(TAG, "GATT error closing connection")
Log.e(TAG, "GATT error, attempting immediate reconnection")
try {
gatt.close()
bluetoothGatt = null
bluetoothLeScanner = null
deviceAddress?.let { address ->
handler.postDelayed({
Log.d(TAG, "Reconnecting to device")
val device =
BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address)
bluetoothGatt = device.connectGatt(
context,
false,
this,
BluetoothDevice.TRANSPORT_LE
)
}, 2000)
if (autoReconnect) {
Log.d(TAG, "Immediate reconnection to device")
handler.post {
val device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address)
bluetoothGatt = device.connectGatt(
context,
false,
this,
BluetoothDevice.TRANSPORT_LE
)
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Reconnect error: ${e.message}")
Log.e(TAG, "Immediate reconnect error: ${e.message}")
}
}
@@ -318,32 +475,42 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
isConnected = true
isReconnecting = false
isManualDisconnect = false
connectionAttempts = 0
Log.i(TAG, "Connected to GATT server")
handler.post { connectionStateCallback?.invoke(true) }
deviceAddress?.let { address ->
handler.post { connectionSuccessCallback?.invoke(address) }
}
handler.postDelayed({
handler.post {
try {
gatt.discoverServices()
} catch (e: Exception) {
Log.e(TAG, "Service discovery failed: ${e.message}")
}
}, 500)
}
}
BluetoothProfile.STATE_DISCONNECTED -> {
isConnected = false
Log.i(TAG, "Disconnected from GATT server")
isReconnecting = false
Log.i(TAG, "Disconnected from GATT server, manual=$isManualDisconnect")
handler.post { connectionStateCallback?.invoke(false) }
handler.post {
connectionStateCallback?.invoke(false)
if (!isManualDisconnect) {
connectionLostCallback?.invoke()
}
}
if (!deviceAddress.isNullOrBlank()) {
handler.postDelayed({
Log.d(TAG, "Reconnecting after disconnect")
connect(deviceAddress!!, connectionStateCallback)
}, 3000)
if (!deviceAddress.isNullOrBlank() && autoReconnect && highFrequencyReconnect && !isManualDisconnect) {
startHighFrequencyReconnect(deviceAddress!!)
} else if (isManualDisconnect) {
Log.d(TAG, "Manual disconnect - no auto reconnect")
}
}
}
@@ -355,17 +522,19 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
private var lastDataTime = 0L
@Suppress("DEPRECATION")
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
super.onCharacteristicChanged(gatt, characteristic)
@Suppress("DEPRECATION")
val newData = characteristic.value?.let {
String(it, StandardCharsets.UTF_8)
} ?: return
Log.d(TAG, "Received data len=${newData.length} preview=${newData.take(50)}")
Log.d(TAG, "Received data len=${newData.length} preview=${newData}")
dataBuffer.append(newData)
@@ -379,18 +548,17 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
val bufferContent = dataBuffer.toString()
val currentTime = System.currentTimeMillis()
if (lastDataTime > 0 && currentTime - lastDataTime > 5000) {
Log.w(TAG, "Data timeout ${(currentTime - lastDataTime) / 1000}s")
if (lastDataTime > 0) {
val timeDiff = currentTime - lastDataTime
if (timeDiff > 10000) {
Log.w(TAG, "Long data gap: ${timeDiff / 1000}s")
}
}
Log.d(TAG, "Buffer size=${dataBuffer.length} bytes")
tryExtractJson(bufferContent)
lastDataTime = currentTime
}
@@ -472,7 +640,7 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
private fun processJsonString(jsonStr: String): Boolean {
try {
val jsonObject = JSONObject(jsonStr)
Log.d(TAG, "Parsed JSON len=${jsonStr.length} preview=${jsonStr.take(50)}")
Log.d(TAG, "Parsed JSON len=${jsonStr.length} preview=${jsonStr}")
handler.post {
@@ -509,9 +677,16 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
)
if (descriptor != null) {
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
val writeResult = gatt.writeDescriptor(descriptor)
Log.d(TAG, "Descriptor write result=$writeResult")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val writeResult = gatt.writeDescriptor(descriptor, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)
Log.d(TAG, "Descriptor write result=$writeResult")
} else {
@Suppress("DEPRECATION")
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
@Suppress("DEPRECATION")
val writeResult = gatt.writeDescriptor(descriptor)
Log.d(TAG, "Descriptor write result=$writeResult")
}
} else {
Log.e(TAG, "Descriptor not found")
@@ -538,10 +713,134 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
private fun requestDataAfterDelay() {
handler.postDelayed({
handler.post {
statusCallback?.let { callback ->
getStatus(callback)
}
}, 1000)
}
}
fun setAutoReconnect(enabled: Boolean) {
autoReconnect = enabled
Log.d(TAG, "Auto reconnect set to: $enabled")
}
fun setHighFrequencyReconnect(enabled: Boolean) {
highFrequencyReconnect = enabled
if (!enabled) {
stopHighFrequencyReconnect()
}
Log.d(TAG, "High frequency reconnect set to: $enabled")
}
fun setConnectionLostCallback(callback: (() -> Unit)?) {
connectionLostCallback = callback
}
fun setConnectionSuccessCallback(callback: ((String) -> Unit)?) {
connectionSuccessCallback = callback
}
fun setSpecifiedDeviceAddress(address: String?) {
specifiedDeviceAddress = address
Log.d(TAG, "Set specified device address: $address")
}
fun getSpecifiedDeviceAddress(): String? = specifiedDeviceAddress
fun setDialogOpen(isOpen: Boolean) {
isDialogOpen = isOpen
Log.d(TAG, "Dialog open state set to: $isOpen")
}
fun setAutoConnectBlocked(blocked: Boolean) {
isAutoConnectBlocked = blocked
Log.d(TAG, "Auto connect blocked set to: $blocked")
}
fun resetManualDisconnectState() {
isManualDisconnect = false
isAutoConnectBlocked = false
Log.d(TAG, "Manual disconnect state reset - auto reconnect enabled")
}
fun setTargetDeviceAddress(address: String?) {
targetDeviceAddress = address
Log.d(TAG, "Set target device address: $address")
}
fun getTargetDeviceAddress(): String? = targetDeviceAddress
private fun startHighFrequencyReconnect(address: String) {
stopHighFrequencyReconnect()
Log.d(TAG, "Starting high frequency reconnect for: $address")
reconnectRunnable = Runnable {
if (!isConnected && autoReconnect && highFrequencyReconnect) {
Log.d(TAG, "High frequency reconnect attempt ${connectionAttempts + 1} for: $address")
connect(address, connectionStateCallback)
if (!isConnected) {
val delay = when {
connectionAttempts < 10 -> 100L
connectionAttempts < 30 -> 200L
connectionAttempts < 60 -> 500L
else -> 1000L
}
reconnectHandler.postDelayed(reconnectRunnable!!, delay)
}
}
}
reconnectHandler.post(reconnectRunnable!!)
}
private fun stopHighFrequencyReconnect() {
reconnectRunnable?.let {
reconnectHandler.removeCallbacks(it)
reconnectRunnable = null
Log.d(TAG, "Stopped high frequency reconnect")
}
}
fun getConnectionAttempts(): Int = connectionAttempts
fun getLastKnownDeviceAddress(): String? = lastKnownDeviceAddress
@SuppressLint("MissingPermission")
fun disconnectAndCleanup() {
isConnected = false
autoReconnect = false
highFrequencyReconnect = false
isManualDisconnect = false
isAutoConnectBlocked = false
stopHighFrequencyReconnect()
stopScan()
bluetoothGatt?.let { gatt ->
try {
gatt.disconnect()
Thread.sleep(200)
gatt.close()
Log.d(TAG, "GATT connection cleaned up")
} catch (e: Exception) {
Log.e(TAG, "Cleanup error: ${e.message}")
}
}
bluetoothGatt = null
bluetoothLeScanner = null
deviceAddress = null
connectionAttempts = 0
dataBuffer.clear()
connectionStateCallback = null
statusCallback = null
trainInfoCallback = null
connectionLostCallback = null
connectionSuccessCallback = null
Log.d(TAG, "BLE client fully disconnected and cleaned up")
}
}

View File

@@ -0,0 +1,123 @@
package org.noxylva.lbjconsole
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import androidx.core.app.NotificationCompat
class BackgroundService : Service() {
companion object {
private const val NOTIFICATION_ID = 1001
private const val CHANNEL_ID = "background_service_channel"
private const val CHANNEL_NAME = "Background Service"
fun startService(context: Context) {
try {
val intent = Intent(context, BackgroundService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
} catch (e: Exception) {
// Service start failed, ignore silently
}
}
fun stopService(context: Context) {
val intent = Intent(context, BackgroundService::class.java)
context.stopService(intent)
}
}
private var wakeLock: PowerManager.WakeLock? = null
override fun onCreate() {
super.onCreate()
createNotificationChannel()
acquireWakeLock()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
try {
val notification = createNotification()
startForeground(NOTIFICATION_ID, notification)
} catch (e: Exception) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
stopSelf()
return START_NOT_STICKY
}
}
return START_STICKY
}
override fun onDestroy() {
super.onDestroy()
releaseWakeLock()
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Keep app running in background"
setShowBadge(false)
}
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
private fun createNotification(): Notification {
val intent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("LBJ Console")
.setContentText("Running in background")
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentIntent(pendingIntent)
.setOngoing(true)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
}
private fun acquireWakeLock() {
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
wakeLock = powerManager.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
"LBJConsole::BackgroundWakeLock"
)
wakeLock?.acquire()
}
private fun releaseWakeLock() {
wakeLock?.let {
if (it.isHeld) {
it.release()
}
}
wakeLock = null
}
}

View File

@@ -0,0 +1,123 @@
package org.noxylva.lbjconsole
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.rememberCoroutineScope
import kotlinx.coroutines.launch
import org.noxylva.lbjconsole.util.DatabaseExportImportUtil
class FilePickerActivity : ComponentActivity() {
private val exportFilePicker = registerForActivityResult(
ActivityResultContracts.CreateDocument("application/json")
) { uri ->
uri?.let { exportDatabase(it) }
}
private val importFilePicker = registerForActivityResult(
ActivityResultContracts.OpenDocument()
) { uri ->
uri?.let { importDatabase(it) }
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val action = intent.getStringExtra("action") ?: "export"
setContent {
val coroutineScope = rememberCoroutineScope()
when (action) {
"export" -> {
val fileName = "lbj_console_backup_${System.currentTimeMillis()}.json"
exportFilePicker.launch(fileName)
}
"import" -> {
importFilePicker.launch(arrayOf("application/json", "text/plain"))
}
}
}
}
private fun exportDatabase(uri: Uri) {
val coroutineScope = kotlinx.coroutines.MainScope()
coroutineScope.launch {
try {
val databaseUtil = DatabaseExportImportUtil(this@FilePickerActivity)
val json = databaseUtil.exportDatabase()
contentResolver.openOutputStream(uri)?.use { outputStream ->
outputStream.write(json.toByteArray())
}
Toast.makeText(
this@FilePickerActivity,
"数据导出成功",
Toast.LENGTH_SHORT
).show()
} catch (e: Exception) {
Toast.makeText(
this@FilePickerActivity,
"导出失败: ${e.message}",
Toast.LENGTH_SHORT
).show()
} finally {
finish()
}
}
}
private fun importDatabase(uri: Uri) {
val coroutineScope = kotlinx.coroutines.MainScope()
coroutineScope.launch {
try {
val databaseUtil = DatabaseExportImportUtil(this@FilePickerActivity)
val success = databaseUtil.importDatabase(uri)
if (success) {
Toast.makeText(
this@FilePickerActivity,
"数据导入成功",
Toast.LENGTH_SHORT
).show()
} else {
Toast.makeText(
this@FilePickerActivity,
"数据导入失败",
Toast.LENGTH_SHORT
).show()
}
} catch (e: Exception) {
Toast.makeText(
this@FilePickerActivity,
"导入失败: ${e.message}",
Toast.LENGTH_SHORT
).show()
} finally {
finish()
}
}
}
companion object {
fun createExportIntent(context: android.content.Context): Intent {
return Intent(context, FilePickerActivity::class.java).apply {
putExtra("action", "export")
}
}
fun createImportIntent(context: android.content.Context): Intent {
return Intent(context, FilePickerActivity::class.java).apply {
putExtra("action", "import")
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,223 @@
package org.noxylva.lbjconsole
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import kotlinx.coroutines.runBlocking
import org.noxylva.lbjconsole.database.AppSettingsRepository
import org.noxylva.lbjconsole.database.TrainDatabase
import android.os.Build
import android.util.Log
import android.view.View
import android.widget.RemoteViews
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import org.json.JSONObject
import org.noxylva.lbjconsole.model.TrainRecord
class NotificationService(private val context: Context) {
companion object {
const val TAG = "NotificationService"
const val CHANNEL_ID = "lbj_messages"
const val CHANNEL_NAME = "LBJ Messages"
const val NOTIFICATION_ID_BASE = 2000
const val PREFS_NAME = "notification_settings"
const val KEY_ENABLED = "notifications_enabled"
}
private val notificationManager = NotificationManagerCompat.from(context)
private val appSettingsRepository = AppSettingsRepository(context)
private var notificationIdCounter = NOTIFICATION_ID_BASE
init {
createNotificationChannel()
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
).apply {
description = "Real-time LBJ train message notifications"
enableVibration(true)
setShowBadge(true)
}
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.createNotificationChannel(channel)
Log.d(TAG, "Notification channel created")
}
}
fun isNotificationEnabled(): Boolean {
return runBlocking {
appSettingsRepository.getSettings().notificationEnabled
}
}
fun setNotificationEnabled(enabled: Boolean) {
runBlocking {
appSettingsRepository.updateNotificationEnabled(enabled)
}
Log.d(TAG, "Notification enabled set to: $enabled")
}
private fun isValidValue(value: String): Boolean {
val trimmed = value.trim()
return trimmed.isNotEmpty() &&
trimmed != "NUL" &&
trimmed != "<NUL>" &&
trimmed != "NA" &&
trimmed != "<NA>" &&
!trimmed.all { it == '*' }
}
fun showTrainNotification(trainRecord: TrainRecord) {
if (!isNotificationEnabled()) {
Log.d(TAG, "Notifications disabled, skipping")
return
}
try {
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
val pendingIntent = PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
)
val remoteViews = RemoteViews(context.packageName, R.layout.notification_train_record)
val trainDisplay = if (isValidValue(trainRecord.lbjClass) && isValidValue(trainRecord.train)) {
"${trainRecord.lbjClass.trim()}${trainRecord.train.trim()}"
} else if (isValidValue(trainRecord.lbjClass)) {
trainRecord.lbjClass.trim()
} else if (isValidValue(trainRecord.train)) {
trainRecord.train.trim()
} else "列车"
remoteViews.setTextViewText(R.id.notification_train_number, trainDisplay)
val directionText = when (trainRecord.direction) {
1 -> ""
3 -> ""
else -> ""
}
if (directionText.isNotEmpty()) {
remoteViews.setTextViewText(R.id.notification_direction, directionText)
remoteViews.setViewVisibility(R.id.notification_direction, View.VISIBLE)
} else {
remoteViews.setViewVisibility(R.id.notification_direction, View.GONE)
}
val locoInfo = when {
isValidValue(trainRecord.locoType) && isValidValue(trainRecord.loco) -> {
val shortLoco = if (trainRecord.loco.length > 5) {
trainRecord.loco.takeLast(5)
} else {
trainRecord.loco
}
"${trainRecord.locoType}-${shortLoco}"
}
isValidValue(trainRecord.locoType) -> trainRecord.locoType
isValidValue(trainRecord.loco) -> trainRecord.loco
else -> ""
}
if (locoInfo.isNotEmpty()) {
remoteViews.setTextViewText(R.id.notification_loco_info, locoInfo)
remoteViews.setViewVisibility(R.id.notification_loco_info, View.VISIBLE)
} else {
remoteViews.setViewVisibility(R.id.notification_loco_info, View.GONE)
}
if (isValidValue(trainRecord.route)) {
remoteViews.setTextViewText(R.id.notification_route, trainRecord.route.trim())
remoteViews.setViewVisibility(R.id.notification_route, View.VISIBLE)
} else {
remoteViews.setViewVisibility(R.id.notification_route, View.GONE)
}
if (isValidValue(trainRecord.position)) {
remoteViews.setTextViewText(R.id.notification_position, "${trainRecord.position.trim().removeSuffix(".")}K")
remoteViews.setViewVisibility(R.id.notification_position, View.VISIBLE)
} else {
remoteViews.setViewVisibility(R.id.notification_position, View.GONE)
}
if (isValidValue(trainRecord.speed)) {
remoteViews.setTextViewText(R.id.notification_speed, "${trainRecord.speed.trim()} km/h")
remoteViews.setViewVisibility(R.id.notification_speed, View.VISIBLE)
} else {
remoteViews.setViewVisibility(R.id.notification_speed, View.GONE)
}
remoteViews.setOnClickPendingIntent(R.id.notification_train_number, pendingIntent)
val summaryParts = mutableListOf<String>()
val routeAndDirection = when {
isValidValue(trainRecord.route) && directionText.isNotEmpty() -> "${trainRecord.route.trim()}${directionText}"
isValidValue(trainRecord.route) -> trainRecord.route.trim()
directionText.isNotEmpty() -> "${directionText}"
else -> null
}
routeAndDirection?.let { summaryParts.add(it) }
if (locoInfo.isNotEmpty()) summaryParts.add(locoInfo)
val summaryText = summaryParts.joinToString("")
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(trainDisplay)
.setContentText(summaryText)
.setCustomContentView(remoteViews)
.setCustomBigContentView(remoteViews)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setWhen(trainRecord.timestamp.time)
.build()
val notificationId = notificationIdCounter++
if (notificationIdCounter > NOTIFICATION_ID_BASE + 1000) {
notificationIdCounter = NOTIFICATION_ID_BASE
}
notificationManager.notify(notificationId, notification)
Log.d(TAG, "Custom notification sent for train: ${trainRecord.train}")
} catch (e: Exception) {
Log.e(TAG, "Failed to show notification: ${e.message}", e)
}
}
fun showTrainNotification(jsonData: JSONObject) {
if (!isNotificationEnabled()) {
Log.d(TAG, "Notifications disabled, skipping")
return
}
try {
val trainRecord = TrainRecord(jsonData)
showTrainNotification(trainRecord)
} catch (e: Exception) {
Log.e(TAG, "Failed to create TrainRecord from JSON: ${e.message}", e)
}
}
fun hasNotificationPermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
notificationManager.areNotificationsEnabled()
} else {
notificationManager.areNotificationsEnabled()
}
}
}

View File

@@ -0,0 +1,31 @@
package org.noxylva.lbjconsole.database
import androidx.room.*
import kotlinx.coroutines.flow.Flow
@Dao
interface AppSettingsDao {
@Query("SELECT * FROM app_settings WHERE id = 1")
suspend fun getSettings(): AppSettingsEntity?
@Query("SELECT * FROM app_settings WHERE id = 1")
fun getSettingsFlow(): Flow<AppSettingsEntity?>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertSettings(settings: AppSettingsEntity)
@Update
suspend fun updateSettings(settings: AppSettingsEntity)
@Query("DELETE FROM app_settings")
suspend fun deleteAllSettings()
@Query("UPDATE app_settings SET notificationEnabled = :enabled WHERE id = 1")
suspend fun updateNotificationEnabled(enabled: Boolean)
@Transaction
suspend fun saveSettings(settings: AppSettingsEntity) {
insertSettings(settings)
}
}

View File

@@ -0,0 +1,26 @@
package org.noxylva.lbjconsole.database
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "app_settings")
data class AppSettingsEntity(
@PrimaryKey val id: Int = 1,
val deviceName: String = "LBJReceiver",
val currentTab: Int = 0,
val historyEditMode: Boolean = false,
val historySelectedRecords: String = "",
val historyExpandedStates: String = "",
val historyScrollPosition: Int = 0,
val historyScrollOffset: Int = 0,
val settingsScrollPosition: Int = 0,
val mapCenterLat: Float? = null,
val mapCenterLon: Float? = null,
val mapZoomLevel: Float = 10.0f,
val mapRailwayLayerVisible: Boolean = true,
val specifiedDeviceAddress: String? = null,
val searchOrderList: String = "",
val autoConnectEnabled: Boolean = true,
val backgroundServiceEnabled: Boolean = false,
val notificationEnabled: Boolean = false
)

View File

@@ -0,0 +1,127 @@
package org.noxylva.lbjconsole.database
import android.content.Context
import android.content.SharedPreferences
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
class AppSettingsRepository(private val context: Context) {
private val dao = TrainDatabase.getDatabase(context).appSettingsDao()
private val sharedPrefs: SharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
suspend fun getSettings(): AppSettingsEntity {
var settings = dao.getSettings()
if (settings == null) {
settings = migrateFromSharedPreferences()
dao.saveSettings(settings)
}
return settings
}
fun getSettingsFlow(): Flow<AppSettingsEntity?> {
return dao.getSettingsFlow()
}
suspend fun saveSettings(settings: AppSettingsEntity) {
dao.saveSettings(settings)
}
suspend fun updateDeviceName(deviceName: String) {
val current = getSettings()
saveSettings(current.copy(deviceName = deviceName))
}
suspend fun updateCurrentTab(tab: Int) {
val current = getSettings()
saveSettings(current.copy(currentTab = tab))
}
suspend fun updateHistoryEditMode(editMode: Boolean) {
val current = getSettings()
saveSettings(current.copy(historyEditMode = editMode))
}
suspend fun updateHistorySelectedRecords(selectedRecords: String) {
val current = getSettings()
saveSettings(current.copy(historySelectedRecords = selectedRecords))
}
suspend fun updateHistoryExpandedStates(expandedStates: String) {
val current = getSettings()
saveSettings(current.copy(historyExpandedStates = expandedStates))
}
suspend fun updateHistoryScrollPosition(position: Int, offset: Int = 0) {
val current = getSettings()
saveSettings(current.copy(historyScrollPosition = position, historyScrollOffset = offset))
}
suspend fun updateSettingsScrollPosition(position: Int) {
val current = getSettings()
saveSettings(current.copy(settingsScrollPosition = position))
}
suspend fun updateMapSettings(centerLat: Float?, centerLon: Float?, zoomLevel: Float, railwayLayerVisible: Boolean) {
val current = getSettings()
saveSettings(current.copy(
mapCenterLat = centerLat,
mapCenterLon = centerLon,
mapZoomLevel = zoomLevel,
mapRailwayLayerVisible = railwayLayerVisible
))
}
suspend fun updateSpecifiedDeviceAddress(address: String?) {
val current = getSettings()
saveSettings(current.copy(specifiedDeviceAddress = address))
}
suspend fun updateSearchOrderList(orderList: String) {
val current = getSettings()
saveSettings(current.copy(searchOrderList = orderList))
}
suspend fun updateAutoConnectEnabled(enabled: Boolean) {
val current = getSettings()
saveSettings(current.copy(autoConnectEnabled = enabled))
}
suspend fun updateBackgroundServiceEnabled(enabled: Boolean) {
val current = getSettings()
saveSettings(current.copy(backgroundServiceEnabled = enabled))
}
suspend fun updateNotificationEnabled(enabled: Boolean) {
val current = getSettings()
saveSettings(current.copy(notificationEnabled = enabled))
}
private fun migrateFromSharedPreferences(): AppSettingsEntity {
return AppSettingsEntity(
deviceName = sharedPrefs.getString("device_name", "LBJReceiver") ?: "LBJReceiver",
currentTab = sharedPrefs.getInt("current_tab", 0),
historyEditMode = sharedPrefs.getBoolean("history_edit_mode", false),
historySelectedRecords = sharedPrefs.getString("history_selected_records", "") ?: "",
historyExpandedStates = sharedPrefs.getString("history_expanded_states", "") ?: "",
historyScrollPosition = sharedPrefs.getInt("history_scroll_position", 0),
historyScrollOffset = sharedPrefs.getInt("history_scroll_offset", 0),
settingsScrollPosition = sharedPrefs.getInt("settings_scroll_position", 0),
mapCenterLat = if (sharedPrefs.contains("map_center_lat")) sharedPrefs.getFloat("map_center_lat", 0f) else null,
mapCenterLon = if (sharedPrefs.contains("map_center_lon")) sharedPrefs.getFloat("map_center_lon", 0f) else null,
mapZoomLevel = sharedPrefs.getFloat("map_zoom_level", 10.0f),
mapRailwayLayerVisible = sharedPrefs.getBoolean("map_railway_layer_visible", true),
specifiedDeviceAddress = sharedPrefs.getString("specified_device_address", null),
searchOrderList = sharedPrefs.getString("search_order_list", "") ?: "",
autoConnectEnabled = sharedPrefs.getBoolean("auto_connect_enabled", true),
backgroundServiceEnabled = sharedPrefs.getBoolean("background_service_enabled", false),
notificationEnabled = context.getSharedPreferences("notification_settings", Context.MODE_PRIVATE)
.getBoolean("notifications_enabled", false)
)
}
suspend fun clearSharedPreferences() {
sharedPrefs.edit().clear().apply()
}
}

View File

@@ -0,0 +1,145 @@
package org.noxylva.lbjconsole.database
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
@Database(
entities = [TrainRecordEntity::class, AppSettingsEntity::class],
version = 4,
exportSchema = false
)
abstract class TrainDatabase : RoomDatabase() {
abstract fun trainRecordDao(): TrainRecordDao
abstract fun appSettingsDao(): AppSettingsDao
companion object {
@Volatile
private var INSTANCE: TrainDatabase? = null
private val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("""
CREATE TABLE IF NOT EXISTS `app_settings` (
`id` INTEGER NOT NULL,
`deviceName` TEXT NOT NULL,
`currentTab` INTEGER NOT NULL,
`historyEditMode` INTEGER NOT NULL,
`historySelectedRecords` TEXT NOT NULL,
`historyExpandedStates` TEXT NOT NULL,
`historyScrollPosition` INTEGER NOT NULL,
`historyScrollOffset` INTEGER NOT NULL,
`settingsScrollPosition` INTEGER NOT NULL,
`mapCenterLat` REAL,
`mapCenterLon` REAL,
`mapZoomLevel` REAL NOT NULL,
`mapRailwayLayerVisible` INTEGER NOT NULL,
`specifiedDeviceAddress` TEXT,
`searchOrderList` TEXT NOT NULL,
`autoConnectEnabled` INTEGER NOT NULL,
`backgroundServiceEnabled` INTEGER NOT NULL,
PRIMARY KEY(`id`)
)
""")
}
}
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE app_settings ADD COLUMN notificationEnabled INTEGER NOT NULL DEFAULT 0")
}
}
val MIGRATION_3_4 = object : Migration(3, 4) {
override fun migrate(database: SupportSQLiteDatabase) {
// Since we can't determine the exact schema change, we'll use fallback migration
// This will preserve data where possible while updating the schema
// Create new table with correct schema
database.execSQL("""
CREATE TABLE IF NOT EXISTS `app_settings_new` (
`id` INTEGER NOT NULL,
`deviceName` TEXT NOT NULL DEFAULT 'LBJReceiver',
`currentTab` INTEGER NOT NULL DEFAULT 0,
`historyEditMode` INTEGER NOT NULL DEFAULT 0,
`historySelectedRecords` TEXT NOT NULL DEFAULT '',
`historyExpandedStates` TEXT NOT NULL DEFAULT '',
`historyScrollPosition` INTEGER NOT NULL DEFAULT 0,
`historyScrollOffset` INTEGER NOT NULL DEFAULT 0,
`settingsScrollPosition` INTEGER NOT NULL DEFAULT 0,
`mapCenterLat` REAL,
`mapCenterLon` REAL,
`mapZoomLevel` REAL NOT NULL DEFAULT 10.0,
`mapRailwayLayerVisible` INTEGER NOT NULL DEFAULT 1,
`specifiedDeviceAddress` TEXT,
`searchOrderList` TEXT NOT NULL DEFAULT '',
`autoConnectEnabled` INTEGER NOT NULL DEFAULT 1,
`backgroundServiceEnabled` INTEGER NOT NULL DEFAULT 0,
`notificationEnabled` INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY(`id`)
)
""")
// Copy data from old table to new table, handling missing columns
try {
database.execSQL("""
INSERT INTO `app_settings_new` (
id, deviceName, currentTab, historyEditMode, historySelectedRecords,
historyExpandedStates, historyScrollPosition, historyScrollOffset,
settingsScrollPosition, mapCenterLat, mapCenterLon, mapZoomLevel,
mapRailwayLayerVisible
)
SELECT
COALESCE(id, 1),
COALESCE(deviceName, 'LBJReceiver'),
COALESCE(currentTab, 0),
COALESCE(historyEditMode, 0),
COALESCE(historySelectedRecords, ''),
COALESCE(historyExpandedStates, ''),
COALESCE(historyScrollPosition, 0),
COALESCE(historyScrollOffset, 0),
COALESCE(settingsScrollPosition, 0),
mapCenterLat,
mapCenterLon,
COALESCE(mapZoomLevel, 10.0),
COALESCE(mapRailwayLayerVisible, 1)
FROM `app_settings`
""")
} catch (e: Exception) {
// If the old table doesn't exist or has different structure, insert default
database.execSQL("""
INSERT INTO `app_settings_new` (
id, deviceName, currentTab, historyEditMode, historySelectedRecords,
historyExpandedStates, historyScrollPosition, historyScrollOffset,
settingsScrollPosition, mapZoomLevel, mapRailwayLayerVisible,
searchOrderList, autoConnectEnabled, backgroundServiceEnabled,
notificationEnabled
) VALUES (
1, 'LBJReceiver', 0, 0, '', '', 0, 0, 0, 10.0, 1, '', 1, 0, 0
)
""")
}
// Drop old table and rename new table
database.execSQL("DROP TABLE IF EXISTS `app_settings`")
database.execSQL("ALTER TABLE `app_settings_new` RENAME TO `app_settings`")
}
}
fun getDatabase(context: Context): TrainDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
TrainDatabase::class.java,
"train_database"
).addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4).build()
INSTANCE = instance
instance
}
}
}
}

View File

@@ -0,0 +1,50 @@
package org.noxylva.lbjconsole.database
import androidx.room.*
import kotlinx.coroutines.flow.Flow
@Dao
interface TrainRecordDao {
@Query("SELECT * FROM train_records ORDER BY timestamp DESC")
suspend fun getAllRecords(): List<TrainRecordEntity>
@Query("SELECT * FROM train_records ORDER BY timestamp DESC")
fun getAllRecordsFlow(): Flow<List<TrainRecordEntity>>
@Query("SELECT * FROM train_records WHERE uniqueId = :uniqueId")
suspend fun getRecordById(uniqueId: String): TrainRecordEntity?
@Query("SELECT COUNT(*) FROM train_records")
suspend fun getRecordCount(): Int
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRecord(record: TrainRecordEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRecords(records: List<TrainRecordEntity>)
@Delete
suspend fun deleteRecord(record: TrainRecordEntity)
@Delete
suspend fun deleteRecords(records: List<TrainRecordEntity>)
@Query("DELETE FROM train_records")
suspend fun deleteAllRecords()
@Query("DELETE FROM train_records WHERE uniqueId = :uniqueId")
suspend fun deleteRecordById(uniqueId: String)
@Query("DELETE FROM train_records WHERE uniqueId IN (:uniqueIds)")
suspend fun deleteRecordsByIds(uniqueIds: List<String>)
@Query("SELECT * FROM train_records WHERE train LIKE '%' || :train || '%' AND route LIKE '%' || :route || '%' AND (:direction = '全部' OR (:direction = '上行' AND direction = 3) OR (:direction = '下行' AND direction = 1)) ORDER BY timestamp DESC")
suspend fun getFilteredRecords(train: String, route: String, direction: String): List<TrainRecordEntity>
@Query("SELECT * FROM train_records ORDER BY timestamp DESC LIMIT :limit")
suspend fun getLatestRecords(limit: Int): List<TrainRecordEntity>
@Query("SELECT * FROM train_records WHERE timestamp >= :fromTime ORDER BY timestamp DESC")
suspend fun getRecordsFromTime(fromTime: Long): List<TrainRecordEntity>
}

View File

@@ -0,0 +1,66 @@
package org.noxylva.lbjconsole.database
import androidx.room.Entity
import androidx.room.PrimaryKey
import org.noxylva.lbjconsole.model.TrainRecord
import org.json.JSONObject
import java.util.*
@Entity(tableName = "train_records")
data class TrainRecordEntity(
@PrimaryKey val uniqueId: String,
val timestamp: Long,
val receivedTimestamp: Long,
val train: String,
val direction: Int,
val speed: String,
val position: String,
val time: String,
val loco: String,
val locoType: String,
val lbjClass: String,
val route: String,
val positionInfo: String,
val rssi: Double
) {
fun toTrainRecord(): TrainRecord {
val jsonData = JSONObject().apply {
put("uniqueId", uniqueId)
put("timestamp", timestamp)
put("receivedTimestamp", receivedTimestamp)
put("train", train)
put("dir", direction)
put("speed", speed)
put("pos", position)
put("time", time)
put("loco", loco)
put("loco_type", locoType)
put("lbj_class", lbjClass)
put("route", route)
put("position_info", positionInfo)
put("rssi", rssi)
}
return TrainRecord(jsonData)
}
companion object {
fun fromTrainRecord(record: TrainRecord): TrainRecordEntity {
return TrainRecordEntity(
uniqueId = record.uniqueId,
timestamp = record.timestamp.time,
receivedTimestamp = record.receivedTimestamp.time,
train = record.train,
direction = record.direction,
speed = record.speed,
position = record.position,
time = record.time,
loco = record.loco,
locoType = record.locoType,
lbjClass = record.lbjClass,
route = record.route,
positionInfo = record.positionInfo,
rssi = record.rssi
)
}
}
}

View File

@@ -0,0 +1,53 @@
package org.noxylva.lbjconsole.model
data class MergeSettings(
val enabled: Boolean = true,
val groupBy: GroupBy = GroupBy.TRAIN_AND_LOCO,
val timeWindow: TimeWindow = TimeWindow.UNLIMITED
)
enum class GroupBy(val displayName: String) {
TRAIN_ONLY("车次号"),
LOCO_ONLY("机车号"),
TRAIN_OR_LOCO("车次号或机车号"),
TRAIN_AND_LOCO("车次号与机车号")
}
enum class TimeWindow(val displayName: String, val seconds: Long?) {
ONE_HOUR("1小时", 3600),
TWO_HOURS("2小时", 7200),
SIX_HOURS("6小时", 21600),
TWELVE_HOURS("12小时", 43200),
ONE_DAY("24小时", 86400),
UNLIMITED("不限时间", null)
}
fun generateGroupKey(record: TrainRecord, groupBy: GroupBy): String? {
return when (groupBy) {
GroupBy.TRAIN_ONLY -> {
val train = record.train.trim()
if (train.isNotEmpty() && train != "<NUL>") train else null
}
GroupBy.LOCO_ONLY -> {
val loco = record.loco.trim()
if (loco.isNotEmpty() && loco != "<NUL>") loco else null
}
GroupBy.TRAIN_OR_LOCO -> {
val train = record.train.trim()
val loco = record.loco.trim()
when {
train.isNotEmpty() && train != "<NUL>" -> train
loco.isNotEmpty() && loco != "<NUL>" -> loco
else -> null
}
}
GroupBy.TRAIN_AND_LOCO -> {
val train = record.train.trim()
val loco = record.loco.trim()
if (train.isNotEmpty() && train != "<NUL>" &&
loco.isNotEmpty() && loco != "<NUL>") {
"${train}_${loco}"
} else null
}
}
}

View File

@@ -0,0 +1,20 @@
package org.noxylva.lbjconsole.model
import java.util.*
data class MergedTrainRecord(
val groupKey: String,
val records: List<TrainRecord>,
val latestRecord: TrainRecord
) {
val recordCount: Int get() = records.size
val timeSpan: Pair<Date, Date> get() =
records.minByOrNull { it.timestamp }!!.timestamp to
records.maxByOrNull { it.timestamp }!!.timestamp
fun getAllCoordinates() = records.mapNotNull { it.getCoordinates() }
fun getUniqueRoutes() = records.map { it.route }.filter { it.isNotEmpty() && it != "<NUL>" }.toSet()
fun getUniquePositions() = records.map { it.position }.filter { it.isNotEmpty() && it != "<NUL>" }.toSet()
}

View File

@@ -1,17 +1,34 @@
package org.noxylva.lbjconsole.model
import android.content.Context
import android.util.Log
import org.json.JSONObject
import java.util.*
import org.osmdroid.util.GeoPoint
import org.noxylva.lbjconsole.util.LocationUtils
import org.noxylva.lbjconsole.util.LocationUtil
import org.noxylva.lbjconsole.util.LocoTypeUtil
class TrainRecord(jsonData: JSONObject? = null) {
companion object {
const val TAG = "TrainRecord"
private var nextId = 0L
private var LocoTypeUtil: LocoTypeUtil? = null
@Synchronized
private fun generateUniqueId(): String {
return "${System.currentTimeMillis()}_${++nextId}"
}
fun initializeLocoTypeUtil(context: Context) {
if (LocoTypeUtil == null) {
LocoTypeUtil = LocoTypeUtil(context)
}
}
}
val uniqueId: String
var timestamp: Date = Date()
var receivedTimestamp: Date = Date()
var train: String = ""
var direction: Int = 0
var speed: String = ""
@@ -28,12 +45,28 @@ class TrainRecord(jsonData: JSONObject? = null) {
private var _coordinates: GeoPoint? = null
init {
uniqueId = if (jsonData?.has("uniqueId") == true) {
jsonData.getString("uniqueId")
} else {
generateUniqueId()
}
jsonData?.let {
try {
if (jsonData.has("timestamp")) {
timestamp = Date(jsonData.getLong("timestamp"))
}
if (jsonData.has("receivedTimestamp")) {
receivedTimestamp = Date(jsonData.getLong("receivedTimestamp"))
} else {
receivedTimestamp = if (jsonData.has("timestamp")) {
Date(jsonData.getLong("timestamp"))
} else {
Date()
}
}
updateFromJson(it)
} catch (e: Exception) {
Log.e(TAG, "Failed to initialize TrainRecord from JSON: ${e.message}")
@@ -51,20 +84,25 @@ class TrainRecord(jsonData: JSONObject? = null) {
position = jsonData.optString("pos", "")
time = jsonData.optString("time", "")
loco = jsonData.optString("loco", "")
locoType = jsonData.optString("loco_type", "")
locoType = if (loco.isNotEmpty()) {
val prefix = if (loco.length >= 3) loco.take(3) else loco
LocoTypeUtil?.getLocoTypeByCode(prefix) ?: ""
} else {
""
}
lbjClass = jsonData.optString("lbj_class", "")
route = jsonData.optString("route", "")
positionInfo = jsonData.optString("position_info", "")
rssi = jsonData.optDouble("rssi", 0.0)
_coordinates = null
Log.d(TAG, "Successfully parsed: train=$train, dir=$direction, speed=$speed")
Log.d(TAG, "Successfully parsed: train=$train, dir=$direction, speed=$speed, lbjClass='$lbjClass', locoType='$locoType'")
} catch (e: Exception) {
Log.e(TAG, "JSON parse error: ${e.message}", e)
try { train = jsonData.optString("train", "") } catch (e: Exception) { }
try { direction = jsonData.optInt("dir", 0) } catch (e: Exception) { }
try { speed = jsonData.optString("speed", "") } catch (e: Exception) { }
@@ -83,7 +121,7 @@ class TrainRecord(jsonData: JSONObject? = null) {
}
_coordinates = LocationUtils.parsePositionInfo(positionInfo)
_coordinates = LocationUtil.parsePositionInfo(positionInfo)
return _coordinates
}
private fun isValidValue(value: String): Boolean {
@@ -96,7 +134,7 @@ class TrainRecord(jsonData: JSONObject? = null) {
!trimmed.all { it == '*' }
}
fun toMap(): Map<String, String> {
fun toMap(showDetailedTime: Boolean = false): Map<String, String> {
val directionText = when (direction) {
1 -> "下行"
3 -> "上行"
@@ -110,23 +148,46 @@ class TrainRecord(jsonData: JSONObject? = null) {
lbjClass.trim()
} else if (isValidValue(train)) {
train.trim()
} else ""
} else null
val map = mutableMapOf<String, String>()
val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault())
map["timestamp"] = dateFormat.format(timestamp)
map["receivedTimestamp"] = dateFormat.format(receivedTimestamp)
trainDisplay?.takeIf { it.isNotEmpty() }?.let { map["train"] = it }
if (trainDisplay.isNotEmpty()) map["train"] = trainDisplay
if (directionText != "未知") map["direction"] = directionText
if (isValidValue(speed)) map["speed"] = "速度: ${speed.trim()} km/h"
if (isValidValue(position)) map["position"] = "位置: ${position.trim()} km"
if (isValidValue(time)) map["time"] = "列车时间: ${time.trim()}"
if (isValidValue(loco)) map["loco"] = "机车号: ${loco.trim()}"
if (isValidValue(locoType)) map["loco_type"] = "型号: ${locoType.trim()}"
if (isValidValue(route)) map["route"] = "线路: ${route.trim()}"
if (isValidValue(positionInfo) && !positionInfo.trim().matches(Regex(".*(<NUL>|\\s)*.*"))) {
map["position_info"] = "位置信息: ${positionInfo.trim()}"
if (isValidValue(speed)) map["speed"] = "${speed.trim()} km/h"
if (isValidValue(position)) {
map["position"] = "${position.trim().removeSuffix(".")} K"
}
if (rssi != 0.0) map["rssi"] = "信号强度: $rssi dBm"
val timeToDisplay = if (showDetailedTime) {
val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault())
if (isValidValue(time)) {
"$time\n${dateFormat.format(receivedTimestamp)}"
} else {
dateFormat.format(receivedTimestamp)
}
} else {
val currentTime = System.currentTimeMillis()
val diffInSec = (currentTime - receivedTimestamp.time) / 1000
when {
diffInSec < 60 -> "${diffInSec}秒前"
diffInSec < 3600 -> "${diffInSec / 60}分钟前"
else -> "${diffInSec / 3600}小时前"
}
}
map["time"] = timeToDisplay
if (isValidValue(loco)) map["loco"] = "${loco.trim()}"
if (isValidValue(locoType)) map["loco_type"] = "${locoType.trim()}"
if (isValidValue(route)) map["route"] = "${route.trim()}"
if (isValidValue(positionInfo) && !positionInfo.trim().matches(Regex(".*(<NUL>|\\s)*.*"))) {
map["position_info"] = "${positionInfo.trim()}"
}
if (rssi != 0.0) map["rssi"] = "$rssi dBm"
return map
}
@@ -134,7 +195,9 @@ class TrainRecord(jsonData: JSONObject? = null) {
fun toJSON(): JSONObject {
val json = JSONObject()
json.put("uniqueId", uniqueId)
json.put("timestamp", timestamp.time)
json.put("receivedTimestamp", receivedTimestamp.time)
json.put("train", train)
json.put("dir", direction)
json.put("speed", speed)
@@ -148,4 +211,14 @@ class TrainRecord(jsonData: JSONObject? = null) {
json.put("rssi", rssi)
return json
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is TrainRecord) return false
return uniqueId == other.uniqueId
}
override fun hashCode(): Int {
return uniqueId.hashCode()
}
}

View File

@@ -4,8 +4,11 @@ import android.content.Context
import android.content.SharedPreferences
import android.os.Environment
import android.util.Log
import kotlinx.coroutines.*
import org.json.JSONArray
import org.json.JSONObject
import org.noxylva.lbjconsole.database.TrainDatabase
import org.noxylva.lbjconsole.database.TrainRecordEntity
import java.io.File
import java.io.FileWriter
import java.text.SimpleDateFormat
@@ -19,15 +22,50 @@ class TrainRecordManager(private val context: Context) {
const val MAX_RECORDS = 1000
private const val PREFS_NAME = "train_records"
private const val KEY_RECORDS = "records"
private const val KEY_MERGE_SETTINGS = "merge_settings"
}
private val trainRecords = CopyOnWriteArrayList<TrainRecord>()
private val recordCount = AtomicInteger(0)
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
private val database = TrainDatabase.getDatabase(context)
private val trainRecordDao = database.trainRecordDao()
private val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
var mergeSettings = MergeSettings()
private set
init {
loadRecords()
ioScope.launch {
migrateFromSharedPreferences()
loadRecords()
loadMergeSettings()
}
}
private suspend fun migrateFromSharedPreferences() {
try {
val jsonStr = prefs.getString(KEY_RECORDS, null)
if (jsonStr != null && jsonStr != "[]") {
val jsonArray = JSONArray(jsonStr)
val records = mutableListOf<TrainRecordEntity>()
for (i in 0 until jsonArray.length()) {
val jsonObject = jsonArray.getJSONObject(i)
val trainRecord = TrainRecord(jsonObject)
records.add(TrainRecordEntity.fromTrainRecord(trainRecord))
}
if (records.isNotEmpty()) {
trainRecordDao.insertRecords(records)
prefs.edit().remove(KEY_RECORDS).apply()
Log.d(TAG, "Migrated ${records.size} records from SharedPreferences to Room database")
}
}
} catch (e: Exception) {
Log.e(TAG, "Failed to migrate records: ${e.message}")
}
}
@@ -38,15 +76,21 @@ class TrainRecordManager(private val context: Context) {
fun addRecord(jsonData: JSONObject): TrainRecord {
val record = TrainRecord(jsonData)
record.receivedTimestamp = Date()
trainRecords.add(0, record)
while (trainRecords.size > MAX_RECORDS) {
trainRecords.removeAt(trainRecords.size - 1)
val removedRecord = trainRecords.removeAt(trainRecords.size - 1)
ioScope.launch {
trainRecordDao.deleteRecordById(removedRecord.uniqueId)
}
}
recordCount.incrementAndGet()
saveRecords()
ioScope.launch {
trainRecordDao.insertRecord(TrainRecordEntity.fromTrainRecord(record))
}
return record
}
@@ -66,6 +110,16 @@ class TrainRecordManager(private val context: Context) {
}
}
suspend fun getFilteredRecordsFromDatabase(): List<TrainRecord> {
return try {
val entities = trainRecordDao.getFilteredRecords(filterTrain, filterRoute, filterDirection)
entities.map { it.toTrainRecord() }
} catch (e: Exception) {
Log.e(TAG, "Failed to get filtered records from database: ${e.message}")
emptyList()
}
}
private fun matchFilter(record: TrainRecord): Boolean {
@@ -108,109 +162,302 @@ class TrainRecordManager(private val context: Context) {
}
suspend fun refreshRecordsFromDatabase() {
try {
val entities = trainRecordDao.getAllRecords()
trainRecords.clear()
entities.forEach { entity ->
trainRecords.add(entity.toTrainRecord())
}
recordCount.set(trainRecords.size)
Log.d(TAG, "Refreshed ${trainRecords.size} records from database")
} catch (e: Exception) {
Log.e(TAG, "Failed to refresh records from database: ${e.message}")
}
}
fun clearRecords() {
trainRecords.clear()
recordCount.set(0)
saveRecords()
ioScope.launch {
trainRecordDao.deleteAllRecords()
}
}
fun deleteRecord(record: TrainRecord): Boolean {
val result = trainRecords.remove(record)
if (result) {
recordCount.decrementAndGet()
saveRecords()
ioScope.launch {
trainRecordDao.deleteRecordById(record.uniqueId)
}
}
return result
}
fun deleteRecords(records: List<TrainRecord>): Int {
var deletedCount = 0
val idsToDelete = mutableListOf<String>()
records.forEach { record ->
if (trainRecords.remove(record)) {
deletedCount++
idsToDelete.add(record.uniqueId)
}
}
if (deletedCount > 0) {
recordCount.addAndGet(-deletedCount)
saveRecords()
ioScope.launch {
trainRecordDao.deleteRecordsByIds(idsToDelete)
}
}
return deletedCount
}
private fun saveRecords() {
try {
val jsonArray = JSONArray()
for (record in trainRecords) {
jsonArray.put(record.toJSON())
ioScope.launch {
try {
val entities = trainRecords.map { TrainRecordEntity.fromTrainRecord(it) }
trainRecordDao.insertRecords(entities)
Log.d(TAG, "Saved ${trainRecords.size} records to database")
} catch (e: Exception) {
Log.e(TAG, "Failed to save records: ${e.message}")
}
prefs.edit().putString(KEY_RECORDS, jsonArray.toString()).apply()
Log.d(TAG, "Saved ${trainRecords.size} records")
} catch (e: Exception) {
Log.e(TAG, "Failed to save records: ${e.message}")
}
}
private fun loadRecords() {
private suspend fun loadRecords() {
try {
val jsonStr = prefs.getString(KEY_RECORDS, "[]")
val jsonArray = JSONArray(jsonStr)
val entities = trainRecordDao.getAllRecords()
trainRecords.clear()
for (i in 0 until jsonArray.length()) {
val jsonObject = jsonArray.getJSONObject(i)
trainRecords.add(TrainRecord(jsonObject))
entities.forEach { entity ->
trainRecords.add(entity.toTrainRecord())
}
recordCount.set(trainRecords.size)
Log.d(TAG, "Loaded ${trainRecords.size} records")
Log.d(TAG, "Loaded ${trainRecords.size} records from database")
} catch (e: Exception) {
Log.e(TAG, "Failed to load records: ${e.message}")
}
}
fun exportToCsv(records: List<TrainRecord>): File? {
try {
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val fileName = "train_records_$timeStamp.csv"
val downloadsDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
val file = File(downloadsDir, fileName)
FileWriter(file).use { writer ->
writer.append("时间戳,列车号,列车类型,方向,速度,位置,时间,机车号,机车类型,路线,位置信息,信号强度\n")
for (record in records) {
val map = record.toMap()
writer.append(map["timestamp"]).append(",")
writer.append(map["train"]).append(",")
writer.append(map["lbj_class"]).append(",")
writer.append(map["direction"]).append(",")
writer.append(map["speed"]?.replace(" km/h", "") ?: "").append(",")
writer.append(map["position"]?.replace(" km", "") ?: "").append(",")
writer.append(map["time"]).append(",")
writer.append(map["loco"]).append(",")
writer.append(map["loco_type"]).append(",")
writer.append(map["route"]).append(",")
writer.append(map["position_info"]).append(",")
writer.append(map["rssi"]?.replace(" dBm", "") ?: "").append("\n")
}
}
return file
} catch (e: Exception) {
Log.e(TAG, "Error exporting to CSV: ${e.message}")
return null
}
}
fun getRecordCount(): Int {
return recordCount.get()
}
fun updateMergeSettings(newSettings: MergeSettings) {
mergeSettings = newSettings
saveMergeSettings()
}
fun getMergedRecords(): List<MergedTrainRecord> {
if (!mergeSettings.enabled) {
return emptyList()
}
val records = getFilteredRecords()
return processRecordsForMerging(records, mergeSettings)
}
fun getMixedRecords(): List<Any> {
if (!mergeSettings.enabled) {
return getFilteredRecords()
}
val allRecords = getFilteredRecords()
val mergedRecords = processRecordsForMerging(allRecords, mergeSettings)
val mergedRecordIds = mergedRecords.flatMap { merged ->
merged.records.map { it.uniqueId }
}.toSet()
val singleRecords = allRecords.filter { record ->
!mergedRecordIds.contains(record.uniqueId)
}
val mixedList = mutableListOf<Any>()
mixedList.addAll(mergedRecords)
mixedList.addAll(singleRecords)
return mixedList.sortedByDescending { item ->
when (item) {
is MergedTrainRecord -> item.latestRecord.timestamp
is TrainRecord -> item.timestamp
else -> Date(0)
}
}
}
private fun processRecordsForMerging(records: List<TrainRecord>, settings: MergeSettings): List<MergedTrainRecord> {
val validRecords = settings.timeWindow.seconds?.let { windowSeconds ->
val currentTime = Date()
records.filter { record ->
(currentTime.time - record.timestamp.time) / 1000 <= windowSeconds
}
} ?: records
return when (settings.groupBy) {
GroupBy.TRAIN_OR_LOCO -> processTrainOrLocoMerging(validRecords)
else -> {
val groupedRecords = mutableMapOf<String, MutableList<TrainRecord>>()
validRecords.forEach { record ->
val groupKey = generateGroupKey(record, settings.groupBy)
if (groupKey != null) {
groupedRecords.getOrPut(groupKey) { mutableListOf() }.add(record)
}
}
groupedRecords.mapNotNull { (groupKey, groupRecords) ->
if (groupRecords.size >= 2) {
val latestRecord = if (groupRecords.size > 1) {
groupRecords.maxByOrNull { it.timestamp } ?: groupRecords.last()
} else {
groupRecords.last()
}
MergedTrainRecord(
groupKey = groupKey,
records = groupRecords.toList(),
latestRecord = latestRecord
)
} else null
}.sortedByDescending { it.latestRecord.timestamp }
}
}
}
private fun processTrainOrLocoMerging(records: List<TrainRecord>): List<MergedTrainRecord> {
val trainGroups = mutableMapOf<String, MutableList<TrainRecord>>()
val locoGroups = mutableMapOf<String, MutableList<TrainRecord>>()
val mergedGroups = mutableSetOf<MutableList<TrainRecord>>()
records.forEach { record ->
val train = record.train.trim()
val loco = record.loco.trim()
if ((train.isEmpty() || train == "<NUL>") && (loco.isEmpty() || loco == "<NUL>")) {
return@forEach
}
var targetGroup: MutableList<TrainRecord>? = null
if (train.isNotEmpty() && train != "<NUL>") {
targetGroup = trainGroups[train]
}
if (targetGroup == null && loco.isNotEmpty() && loco != "<NUL>") {
targetGroup = locoGroups[loco]
}
if (targetGroup != null) {
targetGroup.add(record)
if (train.isNotEmpty() && train != "<NUL>") {
trainGroups[train] = targetGroup
}
if (loco.isNotEmpty() && loco != "<NUL>") {
locoGroups[loco] = targetGroup
}
} else {
val newGroup = mutableListOf(record)
mergedGroups.add(newGroup)
if (train.isNotEmpty() && train != "<NUL>") {
trainGroups[train] = newGroup
}
if (loco.isNotEmpty() && loco != "<NUL>") {
locoGroups[loco] = newGroup
}
}
}
return mergedGroups.mapNotNull { groupRecords ->
if (groupRecords.size >= 2) {
val latestRecord = groupRecords.maxByOrNull { it.timestamp } ?: groupRecords.lastOrNull() ?: return@mapNotNull null
val groupKey = "${latestRecord.train}_OR_${latestRecord.loco}"
MergedTrainRecord(
groupKey = groupKey,
records = groupRecords.toList(),
latestRecord = latestRecord
)
} else null
}.sortedByDescending { it.latestRecord.timestamp }
}
private fun saveMergeSettings() {
ioScope.launch {
try {
val json = JSONObject().apply {
put("enabled", mergeSettings.enabled)
put("groupBy", mergeSettings.groupBy.name)
put("timeWindow", mergeSettings.timeWindow.name)
}
prefs.edit().putString(KEY_MERGE_SETTINGS, json.toString()).apply()
Log.d(TAG, "Saved merge settings")
} catch (e: Exception) {
Log.e(TAG, "Failed to save merge settings: ${e.message}")
}
}
}
private fun loadMergeSettings() {
try {
val jsonStr = prefs.getString(KEY_MERGE_SETTINGS, null)
if (jsonStr != null) {
val json = JSONObject(jsonStr)
mergeSettings = MergeSettings(
enabled = json.getBoolean("enabled"),
groupBy = GroupBy.valueOf(json.getString("groupBy")),
timeWindow = TimeWindow.valueOf(json.getString("timeWindow"))
)
}
Log.d(TAG, "Loaded merge settings: $mergeSettings")
} catch (e: Exception) {
Log.e(TAG, "Failed to load merge settings: ${e.message}")
mergeSettings = MergeSettings()
}
}
suspend fun exportRecordsToJson(): JSONArray {
val jsonArray = JSONArray()
try {
val entities = trainRecordDao.getAllRecords()
entities.forEach { entity ->
val record = entity.toTrainRecord()
jsonArray.put(record.toJSON())
}
Log.d(TAG, "Exported ${entities.size} records to JSON")
} catch (e: Exception) {
Log.e(TAG, "Failed to export records to JSON: ${e.message}")
}
return jsonArray
}
suspend fun importRecordsFromJson(jsonArray: JSONArray): Int {
var importedCount = 0
try {
val records = mutableListOf<TrainRecordEntity>()
for (i in 0 until jsonArray.length()) {
val jsonObject = jsonArray.getJSONObject(i)
val trainRecord = TrainRecord(jsonObject)
records.add(TrainRecordEntity.fromTrainRecord(trainRecord))
}
if (records.isNotEmpty()) {
trainRecordDao.insertRecords(records)
importedCount = records.size
refreshRecordsFromDatabase()
Log.d(TAG, "Imported $importedCount records from JSON")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to import records from JSON: ${e.message}")
}
return importedCount
}
}

View File

@@ -1,172 +0,0 @@
package org.noxylva.lbjconsole.ui.components
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.Marker
import org.noxylva.lbjconsole.model.TrainRecord
@Composable
fun TrainDetailDialog(
trainRecord: TrainRecord,
onDismiss: () -> Unit
) {
val recordMap = trainRecord.toMap()
val coordinates = remember { trainRecord.getCoordinates() }
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true
)
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.verticalScroll(rememberScrollState())
) {
Text(
text = "列车详情",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 16.dp)
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
DetailItem("列车号", recordMap["train"] ?: "--")
DetailItem("方向", recordMap["direction"] ?: "未知")
}
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
DetailItem("接收时间", recordMap["timestamp"] ?: "--")
DetailItem("列车时间", recordMap["time"] ?: "--")
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
DetailItem("速度", recordMap["speed"] ?: "--")
DetailItem("位置", recordMap["position"] ?: "--")
DetailItem("位置信息", recordMap["position_info"] ?: "--")
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
DetailItem("机车号", recordMap["loco"] ?: "--")
DetailItem("机车类型", recordMap["loco_type"] ?: "--")
DetailItem("列车类型", recordMap["lbj_class"] ?: "--")
Divider(modifier = Modifier.padding(vertical = 8.dp))
DetailItem("路线", recordMap["route"] ?: "--")
DetailItem("信号强度", recordMap["rssi"] ?: "--")
if (coordinates != null) {
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
DetailItem(
label = "经纬度",
value = "纬度: ${coordinates.latitude}, 经度: ${coordinates.longitude}"
)
Spacer(modifier = Modifier.height(8.dp))
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.padding(vertical = 8.dp),
contentAlignment = Alignment.Center
) {
AndroidView(
factory = { context ->
MapView(context).apply {
setTileSource(TileSourceFactory.MAPNIK)
setMultiTouchControls(true)
controller.setZoom(15.0)
controller.setCenter(coordinates)
val marker = Marker(this)
marker.position = coordinates
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
marker.title = recordMap["train"] ?: "列车"
overlays.add(marker)
}
},
update = { mapView ->
mapView.controller.setCenter(coordinates)
mapView.invalidate()
}
)
}
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = onDismiss,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp)
) {
Text("关闭")
}
}
}
}
}
@Composable
private fun DetailItem(
label: String,
value: String,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
) {
Text(
text = label,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = value,
style = MaterialTheme.typography.bodyLarge
)
}
}

View File

@@ -1,141 +0,0 @@
package org.noxylva.lbjconsole.ui.components
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.material3.HorizontalDivider
import org.noxylva.lbjconsole.model.TrainRecord
@Composable
fun TrainInfoCard(
trainRecord: TrainRecord,
modifier: Modifier = Modifier
) {
val recordMap = trainRecord.toMap()
Card(
modifier = modifier
.fillMaxWidth()
.padding(vertical = 4.dp, horizontal = 6.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = recordMap["train"]?.toString() ?: "",
fontWeight = FontWeight.Bold,
fontSize = 16.sp
)
Spacer(modifier = Modifier.width(4.dp))
val directionStr = recordMap["direction"]?.toString() ?: ""
val directionColor = when(directionStr) {
"上行" -> MaterialTheme.colorScheme.primary
"下行" -> MaterialTheme.colorScheme.secondary
else -> MaterialTheme.colorScheme.onSurface
}
Surface(
shape = RoundedCornerShape(4.dp),
color = directionColor.copy(alpha = 0.1f),
modifier = Modifier.padding(horizontal = 2.dp)
) {
Text(
text = directionStr,
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp),
fontSize = 12.sp,
color = directionColor
)
}
}
Text(
text = recordMap["timestamp"]?.toString()?.split(" ")?.getOrNull(1) ?: "",
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "速度: ${recordMap["speed"] ?: ""}",
fontSize = 14.sp,
fontWeight = FontWeight.Medium
)
Text(
text = "位置: ${recordMap["position"] ?: ""}",
fontSize = 14.sp,
fontWeight = FontWeight.Medium
)
}
Spacer(modifier = Modifier.height(4.dp))
HorizontalDivider(thickness = 0.5.dp)
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.weight(1f)) {
CompactInfoItem(label = "机车号", value = recordMap["loco"]?.toString() ?: "")
CompactInfoItem(label = "线路", value = recordMap["route"]?.toString() ?: "")
}
Column(modifier = Modifier.weight(1f)) {
CompactInfoItem(label = "类型", value = recordMap["lbj_class"]?.toString() ?: "")
CompactInfoItem(label = "信号", value = recordMap["rssi"]?.toString() ?: "")
}
}
}
}
}
@Composable
private fun CompactInfoItem(
label: String,
value: String,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(vertical = 2.dp)
) {
Text(
text = "$label: ",
fontWeight = FontWeight.Medium,
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = value,
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurface
)
}
}

View File

@@ -1,338 +0,0 @@
package org.noxylva.lbjconsole.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.FilterList
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.noxylva.lbjconsole.model.TrainRecord
import java.text.SimpleDateFormat
import java.util.*
@Composable
fun TrainRecordsList(
records: List<TrainRecord>,
onRecordClick: (TrainRecord) -> Unit,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
if (records.isEmpty()) {
Text(
text = "暂无历史记录",
modifier = Modifier.padding(16.dp),
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(vertical = 4.dp, horizontal = 8.dp)
) {
items(records) { record ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 2.dp)
.clickable { onRecordClick(record) },
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = record.train,
fontWeight = FontWeight.Bold,
fontSize = 15.sp
)
Spacer(modifier = Modifier.width(4.dp))
val directionText = when (record.direction) {
1 -> "下行"
3 -> "上行"
else -> "未知"
}
val directionColor = when(record.direction) {
1 -> MaterialTheme.colorScheme.secondary
3 -> MaterialTheme.colorScheme.primary
else -> MaterialTheme.colorScheme.onSurface
}
Surface(
color = directionColor.copy(alpha = 0.1f),
shape = MaterialTheme.shapes.small
) {
Text(
text = directionText,
modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp),
fontSize = 11.sp,
color = directionColor
)
}
}
Spacer(modifier = Modifier.height(2.dp))
Text(
text = "位置: ${record.position} km",
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Column(
horizontalAlignment = Alignment.End
) {
Text(
text = "${record.speed} km/h",
fontWeight = FontWeight.Medium,
fontSize = 14.sp
)
Spacer(modifier = Modifier.height(2.dp))
val timeStr = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(record.timestamp)
Text(
text = timeStr,
fontSize = 11.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
}
}
}
@Composable
fun TrainRecordsListWithToolbar(
records: List<TrainRecord>,
onRecordClick: (TrainRecord) -> Unit,
onFilterClick: () -> Unit,
onExportClick: () -> Unit,
onClearClick: () -> Unit,
onDeleteRecords: (List<TrainRecord>) -> Unit,
modifier: Modifier = Modifier
) {
var selectedRecords by remember { mutableStateOf<MutableSet<TrainRecord>>(mutableSetOf()) }
var selectionMode by remember { mutableStateOf(false) }
Column(modifier = modifier.fillMaxSize()) {
@OptIn(ExperimentalMaterial3Api::class)
Surface(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.surface,
tonalElevation = 3.dp,
shadowElevation = 3.dp
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = if (selectionMode) "已选择 ${selectedRecords.size}" else "历史记录 (${records.size})",
style = MaterialTheme.typography.titleMedium
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
if (selectionMode) {
TextButton(
onClick = {
if (selectedRecords.isNotEmpty()) {
onDeleteRecords(selectedRecords.toList())
}
selectionMode = false
selectedRecords = mutableSetOf()
},
colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.error
)
) {
Text("删除")
}
TextButton(onClick = {
selectionMode = false
selectedRecords = mutableSetOf()
}) {
Text("取消")
}
} else {
IconButton(onClick = onFilterClick) {
Icon(
imageVector = Icons.Default.FilterList,
contentDescription = "筛选"
)
}
IconButton(onClick = onExportClick) {
Icon(
imageVector = Icons.Default.Share,
contentDescription = "导出"
)
}
}
}
}
}
LazyColumn(
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(vertical = 4.dp, horizontal = 8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items(records.chunked(2)) { rowRecords ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
rowRecords.forEach { record ->
val isSelected = selectedRecords.contains(record)
Card(
modifier = Modifier
.weight(1f)
.clickable {
if (selectionMode) {
if (isSelected) {
selectedRecords.remove(record)
} else {
selectedRecords.add(record)
}
if (selectedRecords.isEmpty()) {
selectionMode = false
}
} else {
onRecordClick(record)
}
}
.padding(vertical = 2.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
if (selectionMode) {
Checkbox(
checked = isSelected,
onCheckedChange = { checked ->
if (checked) {
selectedRecords.add(record)
} else {
selectedRecords.remove(record)
}
if (selectedRecords.isEmpty()) {
selectionMode = false
}
},
modifier = Modifier.padding(end = 8.dp)
)
}
Text(
text = record.train,
fontWeight = FontWeight.Bold,
fontSize = 15.sp,
modifier = Modifier.weight(1f)
)
if (!selectionMode) {
IconButton(
onClick = {
selectionMode = true
selectedRecords = mutableSetOf(record)
},
modifier = Modifier.size(32.dp)
) {
Icon(
imageVector = Icons.Default.Clear,
contentDescription = "删除",
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.error
)
}
}
}
if (record.speed.isNotEmpty() || record.position.isNotEmpty()) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
if (record.speed.isNotEmpty()) {
Text(
text = "${record.speed} km/h",
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
if (record.position.isNotEmpty()) {
Text(
text = "${record.position} km",
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
val timeStr = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(record.timestamp)
Text(
text = timeStr,
fontSize = 11.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
}
}
}
}

View File

@@ -7,7 +7,12 @@ import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.util.Log
import android.view.ViewGroup
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.MyLocation
@@ -16,11 +21,16 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import kotlinx.coroutines.launch
@@ -33,6 +43,9 @@ import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.*
import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
import org.osmdroid.events.MapListener
import org.osmdroid.events.ScrollEvent
import org.osmdroid.events.ZoomEvent
import org.noxylva.lbjconsole.model.TrainRecord
import java.io.File
@@ -41,7 +54,11 @@ import java.io.File
fun MapScreen(
records: List<TrainRecord>,
onCenterMap: () -> Unit = {},
onLocationError: (String) -> Unit = {}
onLocationError: (String) -> Unit = {},
centerPosition: Pair<Double, Double>? = null,
zoomLevel: Double = 10.0,
railwayLayerVisible: Boolean = true,
onStateChange: (Pair<Double, Double>?, Double, Boolean) -> Unit = { _, _, _ -> }
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
@@ -90,7 +107,49 @@ fun MapScreen(
var selectedRecord by remember { mutableStateOf<TrainRecord?>(null) }
var dialogPosition by remember { mutableStateOf<GeoPoint?>(null) }
var railwayLayerVisible by remember { mutableStateOf(true) }
var railwayLayerVisibleState by remember(railwayLayerVisible) { mutableStateOf(railwayLayerVisible) }
fun updateMarkers() {
val mapView = mapViewRef.value ?: return
mapView.overlays.removeAll { it is Marker }
validRecords.forEach { record ->
record.getCoordinates()?.let { point ->
val marker = Marker(mapView).apply {
position = point
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
val recordMap = record.toMap()
title = recordMap["train"]?.toString() ?: "列车"
val latStr = String.format("%.4f", point.latitude)
val lonStr = String.format("%.4f", point.longitude)
val coordStr = "${latStr}°N, ${lonStr}°E"
snippet = coordStr
setInfoWindowAnchor(Marker.ANCHOR_CENTER, 0f)
setOnMarkerClickListener { clickedMarker, _ ->
selectedRecord = record
dialogPosition = point
showDetailDialog = true
true
}
}
mapView.overlays.add(marker)
marker.showInfoWindow()
}
}
mapView.invalidate()
}
LaunchedEffect(records) {
if (isMapInitialized) {
updateMarkers()
}
}
DisposableEffect(lifecycleOwner) {
@@ -128,50 +187,7 @@ fun MapScreen(
}
}
fun updateMarkers() {
val mapView = mapViewRef.value ?: return
mapView.overlays.removeAll { it is Marker }
validRecords.forEach { record ->
record.getCoordinates()?.let { point ->
val marker = Marker(mapView).apply {
position = point
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
val recordMap = record.toMap()
title = recordMap["train"]?.toString() ?: "列车"
val latStr = String.format("%.4f", point.latitude)
val lonStr = String.format("%.4f", point.longitude)
val coordStr = "${latStr}°N, ${lonStr}°E"
snippet = coordStr
setInfoWindowAnchor(Marker.ANCHOR_CENTER, 0f)
setOnMarkerClickListener { clickedMarker, _ ->
selectedRecord = record
dialogPosition = point
showDetailDialog = true
true
}
}
mapView.overlays.add(marker)
marker.showInfoWindow()
}
}
mapView.invalidate()
}
fun updateRailwayLayerVisibility(visible: Boolean) {
@@ -277,14 +293,21 @@ fun MapScreen(
}
if (validRecords.isNotEmpty()) {
validRecords.lastOrNull()?.getCoordinates()?.let { lastPoint ->
controller.setCenter(lastPoint)
controller.setZoom(12.0)
centerPosition?.let { (lat, lon) ->
controller.setCenter(GeoPoint(lat, lon))
controller.setZoom(zoomLevel)
isMapInitialized = true
Log.d("MapScreen", "Map initialized with saved state: lat=$lat, lon=$lon, zoom=$zoomLevel")
} ?: run {
if (validRecords.isNotEmpty()) {
validRecords.lastOrNull()?.getCoordinates()?.let { lastPoint ->
controller.setCenter(lastPoint)
controller.setZoom(12.0)
}
} else {
controller.setCenter(defaultPosition)
controller.setZoom(10.0)
}
} else {
controller.setCenter(defaultPosition)
controller.setZoom(10.0)
}
@@ -292,7 +315,7 @@ fun MapScreen(
val locationProvider = GpsMyLocationProvider(ctx).apply {
locationUpdateMinDistance = 10f
locationUpdateMinTime = 1000
locationUpdateMinTime = 5000
}
@@ -304,30 +327,30 @@ fun MapScreen(
myLocation?.let { location ->
currentLocation = GeoPoint(location.latitude, location.longitude)
if (!isMapInitialized) {
controller.setCenter(location)
controller.setZoom(15.0)
isMapInitialized = true
Log.d("MapScreen", "Map initialized with GPS position: $location")
}
if (!isMapInitialized && centerPosition == null) {
controller.setCenter(location)
controller.setZoom(15.0)
isMapInitialized = true
Log.d("MapScreen", "Map initialized with GPS position: $location")
}
} ?: run {
if (!isMapInitialized) {
if (validRecords.isNotEmpty()) {
validRecords.lastOrNull()?.getCoordinates()?.let { lastPoint ->
controller.setCenter(lastPoint)
controller.setZoom(12.0)
isMapInitialized = true
Log.d("MapScreen", "Map initialized with last record position: $lastPoint")
}
} else {
controller.setCenter(defaultPosition)
isMapInitialized = true
}
}
if (!isMapInitialized && centerPosition == null) {
if (validRecords.isNotEmpty()) {
validRecords.lastOrNull()?.getCoordinates()?.let { lastPoint ->
controller.setCenter(lastPoint)
controller.setZoom(12.0)
isMapInitialized = true
Log.d("MapScreen", "Map initialized with last record position: $lastPoint")
}
} else {
controller.setCenter(defaultPosition)
isMapInitialized = true
}
}
}
} catch (e: Exception) {
e.printStackTrace()
if (!isMapInitialized) {
if (!isMapInitialized && centerPosition == null) {
if (validRecords.isNotEmpty()) {
validRecords.lastOrNull()?.getCoordinates()?.let { lastPoint ->
controller.setCenter(lastPoint)
@@ -357,6 +380,31 @@ fun MapScreen(
setAlignBottom(true)
setLineWidth(2.0f)
}.also { overlays.add(it) }
addMapListener(object : MapListener {
override fun onScroll(event: ScrollEvent?): Boolean {
val center = mapCenter
val zoom = zoomLevelDouble
onStateChange(
center.latitude to center.longitude,
zoom,
railwayLayerVisibleState
)
return true
}
override fun onZoom(event: ZoomEvent?): Boolean {
val center = mapCenter
val zoom = zoomLevelDouble
onStateChange(
center.latitude to center.longitude,
zoom,
railwayLayerVisibleState
)
return true
}
})
} catch (e: Exception) {
e.printStackTrace()
onLocationError("Map component initialization failed: ${e.localizedMessage}")
@@ -381,7 +429,7 @@ fun MapScreen(
coroutineScope.launch {
updateMarkers()
updateRailwayLayerVisibility(railwayLayerVisible)
updateRailwayLayerVisibility(railwayLayerVisibleState)
}
}
)
@@ -430,15 +478,26 @@ fun MapScreen(
FloatingActionButton(
onClick = {
railwayLayerVisible = !railwayLayerVisible
updateRailwayLayerVisibility(railwayLayerVisible)
railwayLayerVisibleState = !railwayLayerVisibleState
updateRailwayLayerVisibility(railwayLayerVisibleState)
mapViewRef.value?.let { mapView ->
val center = mapView.mapCenter
val zoom = mapView.zoomLevelDouble
onStateChange(
center.latitude to center.longitude,
zoom,
railwayLayerVisibleState
)
}
},
modifier = Modifier.size(40.dp),
containerColor = if (railwayLayerVisible)
containerColor = if (railwayLayerVisibleState)
MaterialTheme.colorScheme.primary.copy(alpha = 0.9f)
else
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.9f),
contentColor = if (railwayLayerVisible)
contentColor = if (railwayLayerVisibleState)
MaterialTheme.colorScheme.onPrimary
else
MaterialTheme.colorScheme.onPrimaryContainer
@@ -524,8 +583,8 @@ fun Context.getCompactMarkerDrawable(color: Int): Drawable {
private fun Int.directionText(): String = when (this) {
1 -> ""
3 -> ""
1 -> "下行"
3 -> "上行"
else -> "?"
}
@@ -535,50 +594,143 @@ private fun TrainMarkerDialog(
position: GeoPoint?,
onDismiss: () -> Unit
) {
val recordMap = record.toMap()
val displayItems = recordMap.filterKeys {
it !in setOf("train", "direction", "time")
}.toList()
AlertDialog(
onDismissRequest = onDismiss,
title = {
val recordMap = record.toMap()
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = recordMap["train"]?.toString() ?: "列车", style = MaterialTheme.typography.titleLarge)
Text(
text = recordMap["train"]?.toString() ?: "列车",
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.weight(1f)
)
recordMap["direction"]?.let { direction ->
Spacer(modifier = Modifier.width(8.dp))
Text(
text = direction,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
text = (direction as? Int)?.directionText() ?: direction.toString(),
style = MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 12.sp
),
modifier = Modifier.padding(start = 8.dp)
)
}
}
},
text = {
Column {
record.toMap().forEach { (key, value) ->
if (key != "train" && key != "direction") {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(vertical = 8.dp)
) {
displayItems.forEach { (key, value) ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
val title = when (key) {
"speed" -> "速度"
"position" -> "位置"
"time" -> "时间"
"loco" -> "机车号"
"loco_type" -> "机车型号"
"route" -> "线路"
"rssi" -> "信号强度"
"timestamp" -> "时间"
"receivedTimestamp" -> "接收时间"
else -> key
}
Text(
text = value,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(vertical = 2.dp)
text = title,
style = MaterialTheme.typography.titleMedium
)
Text(
text = value.toString(),
style = MaterialTheme.typography.bodyMedium
)
}
}
position?.let {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "坐标: ${String.format("%.6f", it.latitude)}, ${String.format("%.6f", it.longitude)}",
style = MaterialTheme.typography.bodyMedium
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "坐标信息",
style = MaterialTheme.typography.titleMedium
)
Text(
text = "${String.format("%.6f", it.latitude)}, ${String.format("%.6f", it.longitude)}",
style = MaterialTheme.typography.bodyMedium
)
}
}
}
},
confirmButton = {
TextButton(onClick = onDismiss) {
Text("确定")
Text("关闭")
}
}
)
}
@Composable
private fun InfoSection(title: String, items: List<Pair<String, String>>) {
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 4.dp)
)
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
),
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.padding(12.dp)) {
items.forEach { (key, value) ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 2.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = value.toString(),
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
}
}
@Composable
private fun InfoSectionSimple(title: String, items: List<Pair<String, String>>) {
Column(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium
)
items.forEach { (key, value) ->
Text(
text = value.toString(),
style = MaterialTheme.typography.bodyMedium
)
}
}
}

View File

@@ -1,250 +0,0 @@
package org.noxylva.lbjconsole.ui.screens
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import org.noxylva.lbjconsole.model.TrainRecord
import org.noxylva.lbjconsole.ui.components.TrainDetailDialog
import java.text.SimpleDateFormat
import java.util.*
@Composable
fun MonitorScreen(
latestRecord: TrainRecord?,
recentRecords: List<TrainRecord>,
lastUpdateTime: Date?,
temporaryStatusMessage: String? = null,
onRecordClick: (TrainRecord) -> Unit,
onClearLog: () -> Unit
) {
var showDetailDialog by remember { mutableStateOf(false) }
var selectedRecord by remember { mutableStateOf<TrainRecord?>(null) }
val timeSinceLastUpdate = remember { mutableStateOf<String?>(null) }
LaunchedEffect(key1 = lastUpdateTime) {
if (lastUpdateTime != null) {
while (true) {
val now = Date()
val diffInSec = (now.time - lastUpdateTime.time) / 1000
timeSinceLastUpdate.value = when {
diffInSec < 60 -> "${diffInSec}秒前"
diffInSec < 3600 -> "${diffInSec / 60}分钟前"
else -> "${diffInSec / 3600}小时前"
}
delay(1000)
}
}
}
Box(modifier = Modifier.fillMaxSize().padding(16.dp)) {
Card(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(20.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = timeSinceLastUpdate.value ?: "暂无数据",
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.height(16.dp))
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
if (latestRecord != null) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.clickable {
selectedRecord = latestRecord
showDetailDialog = true
onRecordClick(latestRecord)
}
) {
val recordMap = latestRecord.toMap()
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = recordMap["train"]?.toString() ?: "",
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
color = MaterialTheme.colorScheme.primary
)
Text(
text = recordMap["direction"]?.toString() ?: "",
fontWeight = FontWeight.Bold,
fontSize = 16.sp,
color = when(recordMap["direction"]?.toString()) {
"上行" -> MaterialTheme.colorScheme.primary
"下行" -> MaterialTheme.colorScheme.secondary
else -> MaterialTheme.colorScheme.onSurface
}
)
}
Spacer(modifier = Modifier.height(6.dp))
if (recordMap.containsKey("time")) {
recordMap["time"]?.split("\n")?.forEach { timeLine ->
Text(
text = timeLine,
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(4.dp))
}
}
HorizontalDivider(thickness = 0.5.dp)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
recordMap["speed"]?.let { speed ->
Text(
text = speed,
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurface
)
}
recordMap["position"]?.let { position ->
Text(
text = position,
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurface
)
}
}
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.fillMaxWidth()) {
recordMap.forEach { (key, value) ->
when (key) {
"timestamp", "train", "direction", "time", "speed", "position", "position_info" -> {}
else -> {
Text(
text = value,
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.height(4.dp))
}
}
}
if (recordMap.containsKey("position_info")) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = recordMap["position_info"] ?: "",
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurface
)
}
}
}
}
} else {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
"暂无列车信息",
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.outline
)
if (lastUpdateTime != null) {
Spacer(modifier = Modifier.height(8.dp))
Text(
"上次接收数据: ${SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(lastUpdateTime)}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
)
}
}
}
}
}
}
}
}
if (showDetailDialog && selectedRecord != null) {
TrainDetailDialog(
trainRecord = selectedRecord!!,
onDismiss = { showDetailDialog = false }
)
}
}
@Composable
private fun InfoItem(
label: String,
value: String,
fontSize: TextUnit = 14.sp
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 2.dp)
) {
Text(
text = "$label: ",
fontWeight = FontWeight.Medium,
fontSize = fontSize,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = value,
fontSize = fontSize,
color = MaterialTheme.colorScheme.onSurface
)
}
}

View File

@@ -1,57 +1,505 @@
package org.noxylva.lbjconsole.ui.screens
import android.content.Intent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import org.noxylva.lbjconsole.model.MergeSettings
import org.noxylva.lbjconsole.model.GroupBy
import org.noxylva.lbjconsole.model.TimeWindow
import org.noxylva.lbjconsole.database.AppSettingsRepository
import org.noxylva.lbjconsole.BackgroundService
import org.noxylva.lbjconsole.NotificationService
import org.noxylva.lbjconsole.FilePickerActivity
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.DisposableEffect
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
deviceName: String,
onDeviceNameChange: (String) -> Unit,
onApplySettings: () -> Unit
onApplySettings: () -> Unit,
appVersion: String = "Unknown",
mergeSettings: MergeSettings,
onMergeSettingsChange: (MergeSettings) -> Unit,
scrollPosition: Int = 0,
onScrollPositionChange: (Int) -> Unit = {},
specifiedDeviceAddress: String? = null,
searchOrderList: List<String> = emptyList(),
onSpecifiedDeviceSelected: (String?) -> Unit = {},
autoConnectEnabled: Boolean = true,
onAutoConnectEnabledChange: (Boolean) -> Unit = {}
) {
val uriHandler = LocalUriHandler.current
val scrollState = rememberScrollState(initial = scrollPosition)
DisposableEffect(Unit) {
onDispose {
onScrollPositionChange(scrollState.value)
}
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
.verticalScroll(scrollState)
.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(20.dp)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
),
shape = RoundedCornerShape(16.dp)
) {
OutlinedTextField(
value = deviceName,
onValueChange = onDeviceNameChange,
label = { Text("蓝牙设备名称") },
modifier = Modifier.fillMaxWidth(),
)
Button(
onClick = onApplySettings,
modifier = Modifier.fillMaxWidth()
Column(
modifier = Modifier.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text("应用设置")
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
imageVector = Icons.Default.Bluetooth,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
Text(
"蓝牙设备",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
}
OutlinedTextField(
value = deviceName,
onValueChange = onDeviceNameChange,
label = { Text("设备名称") },
leadingIcon = {
Icon(
imageVector = Icons.Default.DeviceHub,
contentDescription = null
)
},
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp)
)
if (searchOrderList.isNotEmpty()) {
var deviceAddressExpanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
expanded = deviceAddressExpanded,
onExpandedChange = { deviceAddressExpanded = !deviceAddressExpanded }
) {
OutlinedTextField(
value = specifiedDeviceAddress ?: "",
onValueChange = {},
readOnly = true,
label = { Text("指定设备地址") },
leadingIcon = {
Icon(
imageVector = Icons.Default.LocationOn,
contentDescription = null
)
},
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(expanded = deviceAddressExpanded)
},
modifier = Modifier
.fillMaxWidth()
.menuAnchor(),
shape = RoundedCornerShape(12.dp)
)
ExposedDropdownMenu(
expanded = deviceAddressExpanded,
onDismissRequest = { deviceAddressExpanded = false }
) {
DropdownMenuItem(
text = { Text("") },
onClick = {
onSpecifiedDeviceSelected(null)
deviceAddressExpanded = false
}
)
searchOrderList.forEach { address ->
DropdownMenuItem(
text = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(address)
if (address == specifiedDeviceAddress) {
Spacer(modifier = Modifier.width(8.dp))
Icon(
imageVector = Icons.Default.Check,
contentDescription = "已指定",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(16.dp)
)
}
}
},
onClick = {
onSpecifiedDeviceSelected(address)
deviceAddressExpanded = false
}
)
}
}
}
}
}
}
Spacer(modifier = Modifier.weight(1f))
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
),
shape = RoundedCornerShape(16.dp)
) {
Column(
modifier = Modifier.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
Text(
"应用设置",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
}
val context = LocalContext.current
val notificationService = remember(context) { NotificationService(context) }
var backgroundServiceEnabled by remember { mutableStateOf<Boolean?>(null) }
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(context) {
val repository = AppSettingsRepository(context)
backgroundServiceEnabled = repository.getSettings().backgroundServiceEnabled
}
var notificationEnabled by remember(context, notificationService) {
mutableStateOf(notificationService.isNotificationEnabled())
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
"后台保活服务",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium
)
Text(
"保持应用在后台运行",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
if (backgroundServiceEnabled == null) {
CircularProgressIndicator(modifier = Modifier.size(24.dp))
} else {
Switch(
checked = backgroundServiceEnabled!!,
onCheckedChange = { enabled ->
backgroundServiceEnabled = enabled
coroutineScope.launch {
val repository = AppSettingsRepository(context)
repository.updateBackgroundServiceEnabled(enabled)
if (enabled) {
BackgroundService.startService(context)
} else {
BackgroundService.stopService(context)
}
}
}
)
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
"LBJ消息通知",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium
)
Text(
"实时接收列车LBJ消息通知",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Switch(
checked = notificationEnabled,
onCheckedChange = { enabled ->
notificationEnabled = enabled
notificationService.setNotificationEnabled(enabled)
}
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
"自动连接",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium
)
Text(
"自动连接蓝牙设备",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Switch(
checked = autoConnectEnabled,
onCheckedChange = onAutoConnectEnabledChange
)
}
}
}
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
),
shape = RoundedCornerShape(16.dp)
) {
Column(
modifier = Modifier.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
imageVector = Icons.Default.MergeType,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
Text(
"记录合并",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
"启用记录合并",
style = MaterialTheme.typography.bodyMedium
)
Switch(
checked = mergeSettings.enabled,
onCheckedChange = { enabled ->
onMergeSettingsChange(mergeSettings.copy(enabled = enabled))
}
)
}
if (mergeSettings.enabled) {
var groupByExpanded by remember { mutableStateOf(false) }
var timeWindowExpanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
expanded = groupByExpanded,
onExpandedChange = { groupByExpanded = !groupByExpanded }
) {
OutlinedTextField(
value = mergeSettings.groupBy.displayName,
onValueChange = {},
readOnly = true,
label = { Text("分组方式") },
leadingIcon = {
Icon(
imageVector = Icons.Default.Group,
contentDescription = null
)
},
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(expanded = groupByExpanded)
},
modifier = Modifier
.fillMaxWidth()
.menuAnchor(),
shape = RoundedCornerShape(12.dp)
)
ExposedDropdownMenu(
expanded = groupByExpanded,
onDismissRequest = { groupByExpanded = false }
) {
GroupBy.values().forEach { groupBy ->
DropdownMenuItem(
text = { Text(groupBy.displayName) },
onClick = {
onMergeSettingsChange(mergeSettings.copy(groupBy = groupBy))
groupByExpanded = false
}
)
}
}
}
ExposedDropdownMenuBox(
expanded = timeWindowExpanded,
onExpandedChange = { timeWindowExpanded = !timeWindowExpanded }
) {
OutlinedTextField(
value = mergeSettings.timeWindow.displayName,
onValueChange = {},
readOnly = true,
label = { Text("时间窗口") },
leadingIcon = {
Icon(
imageVector = Icons.Default.Schedule,
contentDescription = null
)
},
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(expanded = timeWindowExpanded)
},
modifier = Modifier
.fillMaxWidth()
.menuAnchor(),
shape = RoundedCornerShape(12.dp)
)
ExposedDropdownMenu(
expanded = timeWindowExpanded,
onDismissRequest = { timeWindowExpanded = false }
) {
TimeWindow.values().forEach { timeWindow ->
DropdownMenuItem(
text = { Text(timeWindow.displayName) },
onClick = {
onMergeSettingsChange(mergeSettings.copy(timeWindow = timeWindow))
timeWindowExpanded = false
}
)
}
}
}
}
}
}
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
),
shape = RoundedCornerShape(16.dp)
) {
Column(
modifier = Modifier.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
imageVector = Icons.Default.Storage,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
Text(
"数据管理",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
}
val context = LocalContext.current
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Button(
onClick = {
val intent = FilePickerActivity.createExportIntent(context)
context.startActivity(intent)
},
modifier = Modifier.weight(1f).padding(horizontal = 4.dp)
) {
Icon(Icons.Default.Upload, contentDescription = null)
Spacer(Modifier.width(8.dp))
Text("导出")
}
Button(
onClick = {
val intent = FilePickerActivity.createImportIntent(context)
context.startActivity(intent)
},
modifier = Modifier.weight(1f).padding(horizontal = 4.dp)
) {
Icon(Icons.Default.Download, contentDescription = null)
Spacer(Modifier.width(8.dp))
Text("导入")
}
}
}
}
Text(
text = "LBJ Console v0.0.1 by undef-i",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.clickable {
uriHandler.openUri("https://github.com/undef-i")
}
)
text = "LBJ Console v$appVersion by undef-i",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.clickable {
uriHandler.openUri("https://github.com/undef-i/LBJ_Console")
}
.padding(12.dp)
)
}
}
}

View File

@@ -24,7 +24,7 @@ private val LightColorScheme = lightColorScheme(
)
@Composable
fun LBJReceiverTheme(
fun LBJConsoleTheme(
darkTheme: Boolean = true,
dynamicColor: Boolean = true,

View File

@@ -0,0 +1,67 @@
package org.noxylva.lbjconsole.util
import android.content.Context
import android.net.Uri
import android.util.Log
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.noxylva.lbjconsole.database.AppSettingsEntity
import org.noxylva.lbjconsole.database.TrainDatabase
import org.noxylva.lbjconsole.database.TrainRecordEntity
import java.io.*
import java.util.*
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
class DatabaseExportImportUtil(private val context: Context) {
private val gson = Gson()
private val database = TrainDatabase.getDatabase(context)
data class SimpleRecordBackup(
val records: List<TrainRecordEntity>
)
suspend fun exportDatabase(): String = withContext(Dispatchers.IO) {
try {
val trainRecords = database.trainRecordDao().getAllRecords()
val backup = SimpleRecordBackup(
records = trainRecords
)
val json = gson.toJson(backup)
json
} catch (e: Exception) {
Log.e("DatabaseExport", "导出失败", e)
throw e
}
}
suspend fun importDatabase(uri: Uri): Boolean = withContext(Dispatchers.IO) {
try {
context.contentResolver.openInputStream(uri)?.use { inputStream ->
val json = inputStream.bufferedReader().use { it.readText() }
val backup: SimpleRecordBackup = gson.fromJson(json, object : TypeToken<SimpleRecordBackup>() {}.type)
database.trainRecordDao().deleteAllRecords()
if (backup.records.isNotEmpty()) {
database.trainRecordDao().insertRecords(backup.records)
}
true
} ?: false
} catch (e: Exception) {
Log.e("DatabaseImport", "导入失败", e)
false
}
}
suspend fun getExportFileUri(): Uri {
val filePath = exportDatabase()
return Uri.parse("file://$filePath")
}
}

View File

@@ -4,8 +4,8 @@ import android.util.Log
import org.osmdroid.util.GeoPoint
object LocationUtils {
private const val TAG = "LocationUtils"
object LocationUtil {
private const val TAG = "LocationUtil"
fun parsePositionInfo(positionInfo: String): GeoPoint? {
@@ -52,7 +52,7 @@ object LocationUtils {
val minuteEndIndex = dmsString.indexOf('')
if (minuteEndIndex == -1) {
return degrees
return null
}
val minutes = dmsString.substring(degreeIndex + 1, minuteEndIndex).toDouble()

View File

@@ -0,0 +1,42 @@
package org.noxylva.lbjconsole.util
import android.content.Context
import java.io.BufferedReader
import java.io.InputStreamReader
class LocoTypeUtil(private val context: Context) {
private val locoTypeMap = mutableMapOf<String, String>()
init {
loadLocoTypeMapping()
}
private fun loadLocoTypeMapping() {
try {
context.assets.open("loco_type_info.csv").use { inputStream ->
BufferedReader(InputStreamReader(inputStream)).use { reader ->
reader.lines().forEach { line ->
val parts = line.split(",")
if (parts.size >= 2) {
val code = parts[0].trim()
val type = parts[1].trim()
locoTypeMap[code] = type
}
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
fun getLocoTypeByCode(code: String): String? {
return locoTypeMap[code]
}
fun getLocoTypeByLocoNumber(locoNumber: String): String? {
if (locoNumber.length < 3) return null
val prefix = locoNumber.take(3)
return getLocoTypeByCode(prefix)
}
}

View File

@@ -0,0 +1,66 @@
package org.noxylva.lbjconsole.util
import android.content.Context
import java.io.BufferedReader
import java.io.InputStreamReader
import java.util.regex.Pattern
class TrainTypeUtil(private val context: Context) {
private val trainTypePatterns = mutableListOf<Pair<Pattern, String>>()
init {
loadTrainTypePatterns()
}
private fun loadTrainTypePatterns() {
try {
val inputStream = context.assets.open("train_number_info.csv")
val reader = BufferedReader(InputStreamReader(inputStream))
reader.useLines { lines ->
lines.forEach { line ->
if (line.isNotBlank()) {
val firstQuoteEnd = line.indexOf('"', 1)
if (firstQuoteEnd > 0 && firstQuoteEnd < line.length - 1) {
val regex = line.substring(1, firstQuoteEnd)
val remainingPart = line.substring(firstQuoteEnd + 1).trim()
if (remainingPart.startsWith(",\"") && remainingPart.endsWith("\"")) {
val type = remainingPart.substring(2, remainingPart.length - 1)
try {
val pattern = Pattern.compile(regex)
trainTypePatterns.add(Pair(pattern, type))
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
fun getTrainType(locoType: String, train: String): String? {
if (train.isEmpty()) {
return null
}
val actualTrain = if (locoType == "NA") {
train
} else {
locoType + train
}
for ((pattern, type) in trainTypePatterns) {
if (pattern.matcher(actualTrain).matches()) {
return type
}
}
return null
}
}

0
app/src/main/res/drawable/ic_launcher_background.xml Normal file → Executable file
View File

0
app/src/main/res/drawable/ic_launcher_foreground.xml Normal file → Executable file
View File

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@android:color/white">
<path
android:fillColor="@android:color/white"
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09C6.04,10.33 6,10.66 6,11v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z"/>
</vector>

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,6m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0" />
<path
android:fillColor="#FFFFFF"
android:pathData="M12,10L12,10c-2.2,0 -4,1.8 -4,4v6h8v-6C16,11.8 14.2,10 12,10z" />
</vector>

View File

@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="4dp"
android:background="@android:color/transparent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="4dp"
android:gravity="center_vertical">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/notification_train_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="G1234"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:layout_marginEnd="4dp" />
<TextView
android:id="@+id/notification_direction"
android:layout_width="16dp"
android:layout_height="16dp"
android:text="下"
android:textSize="10sp"
android:textStyle="bold"
android:textColor="@android:color/white"
android:background="@android:color/black"
android:gravity="center"
android:visibility="gone" />
</LinearLayout>
<TextView
android:id="@+id/notification_loco_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="CRH380D-1234"
android:textSize="12sp"
android:textColor="?android:attr/textColorPrimary" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/notification_route"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="京沪高铁"
android:textSize="14sp"
android:textColor="?android:attr/textColorPrimary"
android:layout_marginEnd="4dp" />
<TextView
android:id="@+id/notification_position"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1234K"
android:textSize="14sp"
android:textColor="?android:attr/textColorPrimary" />
</LinearLayout>
<TextView
android:id="@+id/notification_speed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="300 km/h"
android:textSize="14sp"
android:textColor="?android:attr/textColorPrimary" />
</LinearLayout>
</LinearLayout>

0
app/src/main/res/mipmap-anydpi/ic_launcher.xml Normal file → Executable file
View File

0
app/src/main/res/mipmap-anydpi/ic_launcher_round.xml Normal file → Executable file
View File

0
app/src/main/res/mipmap-hdpi/ic_launcher.webp Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

0
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

0
app/src/main/res/mipmap-mdpi/ic_launcher.webp Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 982 B

After

Width:  |  Height:  |  Size: 982 B

0
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

0
app/src/main/res/mipmap-xhdpi/ic_launcher.webp Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

0
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

0
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

0
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

0
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

0
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -1,590 +0,0 @@
6G,51,90,西安铁路局 宝鸡电力机务段,,
6K,1,85,中国铁路郑州局集团有限公司 洛阳机务段,,
8G,1,1,中国铁路太原局集团有限公司 太原北机务段、侯马机务段、石家庄电力机务段,,
8G,2,2,中国铁道博物馆,,
8G,3,75,中国铁路太原局集团有限公司 太原北机务段、侯马机务段、石家庄电力机务段,,
8G,76,76,中国铁路太原局集团有限公司 太原机务段北场,,
8G,77,96,中国铁路太原局集团有限公司 太原北机务段、侯马机务段、石家庄电力机务段,,
8G,97,97,中国铁路太原局集团有限公司 榆次机务折返段,,
8G,98,100,中国铁路太原局集团有限公司 太原北机务段、侯马机务段、石家庄电力机务段,,
8K,1,1,中国铁路北京局集团有限公司 丰台机务段,,
8K,2,7,*北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段,,
8K,8,8,中国铁道博物馆,*科技号,
8K,9,17,*北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段,,
8K,18,18,*北京铁路局 丰台机务段,,
8K,19,23,*北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段,,
8K,24,24,中国铁路太原局集团有限公司 湖东机务段 大同西运用车间,,
8K,25,64,*北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段,,
8K,65,65,天津铁道职业技术学院,,
8K,66,71,*北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段,,
8K,72,72,中国铁路北京局集团有限公司 丰台机务段,,
8K,73,90,*北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段,,
8K,91,91,中国铁路太原局集团有限公司 太原机务段北场 机车展场,,
8K,92,100,*北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段,,
CR400AF,21,21,中国铁路北京局集团有限公司 朝阳动车运用所,CR400AF-G,
CR400AF,207,208,中国铁路北京局集团有限公司 北京西动车运用所,,
CR400AF,1001,1002,中国铁路广州局集团有限公司 深圳动车运用所,CR400AF-A,
CR400AF,1003,1003,中国铁路广州局集团有限公司 广州南动车运用所,CR400AF-A,
CR400AF,1004,1004,中国铁路广州局集团有限公司 深圳动车运用所,CR400AF-A,
CR400AF,1005,1005,中国铁路广州局集团有限公司 广州南动车运用所,CR400AF-A,
CR400AF,1006,1006,中国铁路广州局集团有限公司 广州南动车运用所,,
CR400AF,1007,1009,中国铁路广州局集团有限公司 潮州动车运用所,,
CR400AF,1010,1010,中国铁路广州局集团有限公司 广州南动车运用所,,
CR400AF,1011,1014,中国铁路广州局集团有限公司 潮州动车运用所,,
CR400AF,1015,1020,中国铁路广州局集团有限公司 广州南动车运用所,,
CR400AF,1021,1021,中国铁路广州局集团有限公司 潮州动车运用所,,
CR400AF,1022,1025,中国铁路广州局集团有限公司 广州南动车运用所,,
CR400AF,1026,1027,中国铁路广州局集团有限公司 广州南动车运用所,CR400AF-A,
CR400AF,1028,1029,中国铁路广州局集团有限公司 深圳动车运用所,CR400AF-A,
CR400AF,1030,1030,中国铁路广州局集团有限公司 广州南动车运用所,CR400AF-A,
CR400AF,1031,1031,中国铁路广州局集团有限公司 深圳动车运用所,CR400AF-A,
CR400AF,1032,1032,中国铁路广州局集团有限公司 广州南动车运用所,CR400AF-A,
CR400AF,1033,1033,中国铁路广州局集团有限公司 深圳动车运用所,CR400AF-A,
CR400AF,1034,1038,中国铁路广州局集团有限公司 广州南动车运用所,CR400AF-A,
CR400AF,1039,1039,中国铁路广州局集团有限公司 潮州动车运用所,,
CR400AF,1040,1040,中国铁路广州局集团有限公司 广州南动车运用所,,
CR400AF,2002,2002,中国铁路北京局集团有限公司 北京西动车运用所,,
CR400AF,2004,2004,中国铁路北京局集团有限公司 北京西动车运用所,,
CR400AF,2005,2005,中国铁路成都局集团有限公司 重庆西动车运用所,,
CR400AF,2006,2007,中国铁路北京局集团有限公司 北京西动车运用所,,
CR400AF,2008,2008,中国铁路北京局集团有限公司 雄安动车运用所,,
CR400AF,2009,2010,中国铁路北京局集团有限公司 北京西动车运用所,,
CR400AF,2011,2011,中国铁路广州局集团有限公司 长沙动车运用所,,
CR400AF,2012,2012,中国铁路北京局集团有限公司 北京西动车运用所,,
CR400AF,2013,2013,中国铁路成都局集团有限公司 重庆西动车运用所,,
CR400AF,2014,2016,中国铁路北京局集团有限公司 北京西动车运用所,,
CR400AF,2017,2017,中国铁路广州局集团有限公司 长沙动车运用所,,
CR400AF,2023,2023,中国铁路北京局集团有限公司 北京西动车运用所,,
CR400AF,2024,2024,中国铁路广州局集团有限公司 长沙动车运用所,,
CR400AF,2025,2025,中国铁路成都局集团有限公司 重庆西动车运用所,,
CR400AF,2026,2028,中国铁路广州局集团有限公司 长沙动车运用所,,
CR400AF,2030,2030,中国铁路北京局集团有限公司 雄安动车运用所,,
CR400AF,2031,2032,中国铁路成都局集团有限公司 重庆西动车运用所,,
CR400AF,2033,2033,中国铁路北京局集团有限公司 雄安动车运用所,,
CR400AF,2034,2034,中国铁路北京局集团有限公司 北京西动车运用所,,
CR400AF,2035,2038,中国铁路广州局集团有限公司 长沙动车运用所,,
CR400AF,2040,2046,中国铁路广州局集团有限公司 长沙动车运用所,,
CR400AF,2047,2048,中国铁路北京局集团有限公司 北京西动车运用所,,
CR400AF,2049,2049,中国铁路成都局集团有限公司 重庆西动车运用所,,
CR400AF,2051,2051,中国铁路广州局集团有限公司 长沙动车运用所,,
CR400AF,2053,2055,中国铁路广州局集团有限公司 长沙动车运用所,,
CR400AF,2057,2057,中国铁路广州局集团有限公司 长沙动车运用所,,
CR400AF,2058,2058,中国铁路北京局集团有限公司 雄安动车运用所,,
CR400AF,2060,2062,中国铁路广州局集团有限公司 长沙动车运用所,,
CR400AF,2064,2064,中国铁路广州局集团有限公司 长沙动车运用所,,
CR400AF,2065,2066,中国铁路广州局集团有限公司 深圳动车运用所,CR400AF-A,
CR400AF,2067,2068,中国铁路广州局集团有限公司 长沙动车运用所,CR400AF-A,
CR400AF,2069,2069,中国铁路广州局集团有限公司 深圳动车运用所,CR400AF-A,
CR400AF,2070,2070,中国铁路广州局集团有限公司 长沙动车运用所,CR400AF-A,
CR400AF,2071,2071,中国铁路广州局集团有限公司 深圳动车运用所,CR400AF-A,
CR400AF,2072,2072,中国铁路广州局集团有限公司 长沙动车运用所,CR400AF-A,
CR400AF,2073,2073,中国铁路广州局集团有限公司 深圳动车运用所,CR400AF-A,
CR400AF,2074,2076,中国铁路广州局集团有限公司 长沙动车运用所,CR400AF-A,
CR400AF,2077,2079,中国铁路广州局集团有限公司 深圳动车运用所,CR400AF-A,
CR400AF,2080,2084,中国铁路广州局集团有限公司 长沙动车运用所,CR400AF-A,
CR400AF,2085,2085,中国铁路济南局集团有限公司 济南东动车运用所,,
CR400AF,2086,2086,中国铁路济南局集团有限公司 青岛动车运用所,,
CR400AF,2087,2087,中国铁路济南局集团有限公司 济南东动车运用所,,
CR400AF,2088,2090,中国铁路济南局集团有限公司 青岛动车运用所,,
CR400AF,2091,2094,中国铁路济南局集团有限公司 济南东动车运用所,,
CR400AF,2095,2097,中国铁路广州局集团有限公司 长沙动车运用所,CR400AF-A,
CR400AF,2098,2098,中国铁路广州局集团有限公司 深圳动车运用所,CR400AF-A,
CR400AF,2099,2100,中国铁路广州局集团有限公司 长沙动车运用所,CR400AF-A,
CR400AF,2102,2102,中国铁路济南局集团有限公司 济南东动车运用所,CR400AF-A,
CR400AF,2103,2104,中国铁路广州局集团有限公司 长沙动车运用所,CR400AF-A,
CR400AF,2105,2105,中国铁路广州局集团有限公司 深圳动车运用所,CR400AF-A,
CR400AF,2106,2106,中国铁路广州局集团有限公司 长沙动车运用所,CR400AF-A,
CR400AF,2107,2115,中国铁路济南局集团有限公司 济南东动车运用所,CR400AF-A,
CR400AF,2116,2123,中国铁路北京局集团有限公司 北京南动车运用所,CR400AF-B,
CR400AF,2124,2124,中国铁路武汉局集团有限公司 武汉动车运用所,,
CR400AF,2125,2125,中国铁路武汉局集团有限公司 武汉动车运用所,,
CR400AF,2126,2127,中国铁路武汉局集团有限公司 汉口动车运用所,,
CR400AF,2128,2128,中国铁路武汉局集团有限公司 武汉动车运用所,,
CR400AF,2130,2131,中国铁路广州局集团有限公司 长沙动车运用所,,
CR400AF,2133,2133,中国铁路广州局集团有限公司 长沙动车运用所,,
CR400AF,2134,2134,中国铁路济南局集团有限公司 青岛动车运用所,,
CR400AF,2135,2135,中国铁路成都局集团有限公司 重庆西动车运用所,,
CR400AF,2136,2138,中国铁路济南局集团有限公司 青岛动车运用所,,
CR400AF,2139,2139,中国铁路济南局集团有限公司 济南东动车运用所,,
CR400AF,2140,2140,中国铁路成都局集团有限公司 重庆西动车运用所,,
CR400AF,2141,2141,中国铁路济南局集团有限公司 济南东动车运用所,,
CR400AF,2142,2144,中国铁路北京局集团有限公司 北京西动车运用所,,
CR400AF,2145,2146,中国铁路北京局集团有限公司 雄安动车运用所,,
CR400AF,2148,2150,中国铁路武汉局集团有限公司 汉口动车运用所,,
CR400AF,2151,2151,中国铁路武汉局集团有限公司 武汉动车运用所,,
CR400AF,2152,2153,中国铁路武汉局集团有限公司 汉口动车运用所,,
CR400AF,2154,2156,中国铁路武汉局集团有限公司 武汉动车运用所,,
CR400AF,2159,2159,中国铁路武汉局集团有限公司 武汉动车运用所,,
CR400AF,2160,2161,中国铁路武汉局集团有限公司 汉口动车运用所,,
CR400AF,2162,2163,中国铁路济南局集团有限公司 济南东动车运用所,,
CR400AF,2164,2164,中国铁路武汉局集团有限公司 武汉动车运用所,,
CR400AF,2171,2172,中国铁路武汉局集团有限公司 武汉动车运用所,,
CR400AF,2173,2173,中国铁路武汉局集团有限公司 汉口动车运用所,,
CR400AF,2174,2177,中国铁路武汉局集团有限公司 武汉动车运用所,,
CR400AF,2178,2178,中国铁路北京局集团有限公司 雄安动车运用所,,
CR400AF,2179,2179,中国铁路北京局集团有限公司 北京西动车运用所,,
CR400AF,2180,2180,中国铁路北京局集团有限公司 雄安动车运用所,,
CR400AF,2181,2182,中国铁路北京局集团有限公司 北京西动车运用所,,
CR400AF,2183,2187,中国铁路北京局集团有限公司 北京南动车运用所,,
CR400AF,2190,2192,中国铁路武汉局集团有限公司 武汉动车运用所,CR400AF-A,
CR400AF,2193,2193,中国铁路济南局集团有限公司 济南东动车运用所,CR400AF-A,
CR400AF,2194,2195,中国铁路广州局集团有限公司 深圳动车运用所,CR400AF-A,
CR400AF,2196,2200,中国铁路武汉局集团有限公司 武汉动车运用所,CR400AF-A,
CR400AF,2201,2205,中国铁路济南局集团有限公司 济南东动车运用所,CR400AF-A,
CR400AF,2206,2210,中国铁路北京局集团有限公司 北京南动车运用所,CR400AF-B,
CR400AF,2211,2212,中国铁路济南局集团有限公司 济南东动车运用所,CR400AF-A,
CR400AF,2213,2213,中国铁路北京局集团有限公司 北京西动车运用所,,
CR400AF,2215,2217,中国铁路北京局集团有限公司 朝阳动车运用所,CR400AF-G,
CR400AF,2222,2225,中国铁路上海局集团有限公司 上海南动车运用所,,
CR400AF,2226,2226,中国铁路广州局集团有限公司 广州南动车运用所,,
CR400AF,2227,2227,中国铁路广州局集团有限公司 长沙动车运用所,,
CR400AF,2228,2228,中国铁路广州局集团有限公司 广州南动车运用所,,
CR400AF,2229,2229,中国铁路广州局集团有限公司 长沙动车运用所,,
CR400AF,2230,2231,中国铁路济南局集团有限公司 济南东动车运用所,,
CR400AF,2232,2235,中国铁路上海局集团有限公司 上海南动车运用所,,
CR400AF,2236,2236,中国铁路成都局集团有限公司 重庆西动车运用所,,
CR400AF,2237,2243,中国铁路上海局集团有限公司 上海南动车运用所,,
CR400AF,2244,2248,中国铁路成都局集团有限公司 重庆西动车运用所,,
CR400AF,2254,2256,中国铁路成都局集团有限公司 重庆西动车运用所,,
DJ1,1,1,中国铁道科学研究院 环形铁道,,
DJ1,2,2,株洲西门子牵引设备有限公司,,
DJ1,3,3,西安铁路局 宝鸡机务段 秦岭附加队 ,,
DJ2,1,1,中国铁路郑州局集团有限公司 郑州机务段京武快车队,奥星,
DJ2,2,3,中国铁路郑州局集团有限公司 郑州机务段,奥星,
HXD1D,1,15,中国铁路武汉局集团有限公司 武昌南机务段,,
HXD1D,16,16,中国铁路上海局集团有限公司 杭州机务段,,
HXD1D,17,17,中国铁路南昌局集团有限公司 南昌机务段,,
HXD1D,18,18,中国铁路南昌局集团有限公司 鹰潭机务段,,
HXD1D,19,19,中国铁路上海局集团有限公司 杭州机务段,,
HXD1D,20,20,中国铁路南昌局集团有限公司 南昌机务段,,
HXD1D,21,21,中国铁路上海局集团有限公司 杭州机务段,,
HXD1D,22,24,中国铁路南昌局集团有限公司 南昌机务段,,
HXD1D,25,25,中国铁路南昌局集团有限公司 鹰潭机务段,,
HXD1D,26,26,中国铁路南昌局集团有限公司 南昌机务段,,
HXD1D,27,27,中国铁路上海局集团有限公司 杭州机务段,,
HXD1D,28,28,中国铁路南昌局集团有限公司 鹰潭机务段,,
HXD1D,29,34,中国铁路南昌局集团有限公司 南昌机务段,,
HXD1D,35,35,中国铁路上海局集团有限公司 杭州机务段,,
HXD1D,36,38,中国铁路兰州局集团有限公司 兰州西机务段,,
HXD1D,39,39,中国铁路济南局集团有限公司 济南机务段,,
HXD1D,40,50,中国铁路兰州局集团有限公司 兰州西机务段,,
HXD1D,51,75,中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段,,
HXD1D,76,105,中国铁路兰州局集团有限公司 兰州西机务段,,
HXD1D,106,137,中国铁路上海局集团有限公司 上海机务段,,
HXD1D,138,168,中国铁路上海局集团有限公司 杭州机务段,,
HXD1D,169,175,中国铁路上海局集团有限公司 上海机务段,,
HXD1D,176,185,中国铁路武汉局集团有限公司 武昌南机务段,,
HXD1D,186,187,中国铁路南昌局集团有限公司 鹰潭机务段,,
HXD1D,188,188,中国铁路南昌局集团有限公司 南昌机务段,,
HXD1D,189,190,中国铁路南昌局集团有限公司 鹰潭机务段,,
HXD1D,191,232,中国铁路南昌局集团有限公司 南昌机务段,,
HXD1D,233,233,中国铁路南昌局集团有限公司 鹰潭机务段,,
HXD1D,234,237,中国铁路南昌局集团有限公司 南昌机务段,,
HXD1D,238,257,中国铁路广州局集团有限公司 广州机务段,,
HXD1D,258,270,中国铁路武汉局集团有限公司 武昌南机务段,,
HXD1D,271,275,中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段,,
HXD1D,276,279,中国铁路上海局集团有限公司 上海机务段,,
HXD1D,280,289,中国铁路上海局集团有限公司 徐州机务段,,
HXD1D,290,291,中国铁路南昌局集团有限公司 南昌机务段,,
HXD1D,292,293,中国铁路南昌局集团有限公司 鹰潭机务段,,
HXD1D,294,295,中国铁路南昌局集团有限公司 南昌机务段,,
HXD1D,296,300,中国铁路广州局集团有限公司 广州机务段,,
HXD1D,301,310,中国铁路武汉局集团有限公司 武昌南机务段,,
HXD1D,311,320,中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段,,
HXD1D,321,340,中国铁路青藏集团有限公司 西宁机务段,,
HXD1D,341,362,中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段,,
HXD1D,363,382,中国铁路广州局集团有限公司 广州机务段,,
HXD1D,383,392,中国铁路南昌局集团有限公司 南昌机务段,,
HXD1D,393,405,中国铁路上海局集团有限公司 上海机务段,,
HXD1D,406,415,中国铁路兰州局集团有限公司 兰州西机务段,,
HXD1D,416,430,中国铁路广州局集团有限公司 长沙机务段,,
HXD1D,431,440,中国铁路南昌局集团有限公司 南昌机务段,,
HXD1D,441,445,中国铁路南昌局集团有限公司 南昌机务段,,
HXD1D,446,450,中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段,,
HXD1D,451,460,中国铁路武汉局集团有限公司 武昌南机务段,,
HXD1D,461,470,中国铁路广州局集团有限公司 广州机务段,,
HXD1D,471,478,中国铁路兰州局集团有限公司 兰州西机务段,,
HXD1D,479,483,中国铁路上海局集团有限公司 上海机务段,,
HXD1D,484,488,中国铁路上海局集团有限公司 杭州机务段,,
HXD1D,489,490,中国铁路上海局集团有限公司 杭州机务段,,
HXD1D,491,510,中国铁路郑州局集团有限公司 郑州机务段,,
HXD1D,511,512,中国铁路上海局集团有限公司 徐州机务段,,
HXD1D,513,515,中国铁路上海局集团有限公司 上海机务段,,
HXD1D,516,520,中国铁路南昌局集团有限公司 南昌机务段,,
HXD1D,521,534,中国铁路广州局集团有限公司 广州机务段,,
HXD1D,522,522,广州铁路职业技术学院,,
HXD1D,535,544,中国铁路南昌局集团有限公司 南昌机务段,,
HXD1D,545,551,中国铁路上海局集团有限公司 上海机务段,,
HXD1D,552,554,中国铁路上海局集团有限公司 杭州机务段,,
HXD1D,555,559,中国铁路郑州局集团有限公司 郑州机务段,,
HXD1D,560,564,中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段,,
HXD1D,565,570,中国铁路广州局集团有限公司 广州机务段,,
HXD1D,571,585,中国铁路南昌局集团有限公司 南昌机务段,,
HXD1D,586,595,中国铁路上海局集团有限公司 杭州机务段,,
HXD1D,596,613,中国铁路兰州局集团有限公司 兰州西机务段,,
HXD1D,614,623,中国铁路广州局集团有限公司 广州机务段,,
HXD1D,624,633,中国铁路上海局集团有限公司 上海机务段,,
HXD1D,634,636,中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段,,
HXD1D,637,644,中国铁路郑州局集团有限公司 郑州机务段,,
HXD1D,645,660,中国铁路郑州局集团有限公司 郑州机务段,,
HXD1D,661,668,中国铁路武汉局集团有限公司 武昌南机务段,,
HXD1D,669,673,中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段,,
HXD1D,674,678,中国铁路郑州局集团有限公司 郑州机务段,,
HXD1D,679,682,中国铁路上海局集团有限公司 上海机务段,,
HXD1D,683,683,中国铁路上海局集团有限公司 徐州机务段,,
HXD1D,684,684,中国铁路上海局集团有限公司 徐州机务段,,
HXD1D,685,689,中国铁路青藏集团有限公司 格尔木机务段,,
HXD1D,1898,1898,中国铁路上海局集团有限公司 上海机务段,周恩来号,
HXD1D-J,1,3,中国铁路青藏集团有限公司 拉萨动车运用所,,
HXD1D-J,1001,1009,中国铁路昆明局集团有限公司 昆明动车运用所,,
HXD1D-J,1010,1013,中国铁路青藏集团有限公司 格尔木机务段,,
HXD1D-J,1014,1019,中国铁路成都局集团有限公司 成都动车运用所,,
HXD1D-J,1020,1027,中国铁路昆明局集团有限公司 昆明动车运用所,,
HXD3C,1,9,中国铁路沈阳局集团有限公司 沈阳机务段,,
HXD3C,10,10,中国铁路沈阳局集团有限公司 沈阳机务段,,
HXD3C,11,15,中国铁路济南局集团有限公司 济南机务段,,
HXD3C,16,20,中国铁路武汉局集团有限公司 江岸机务段(襄阳机务段支配),,
HXD3C,21,25,中国铁路南昌局集团有限公司 南昌机务段,,
HXD3C,26,30,中国铁路郑州局集团有限公司 郑州机务段,,
HXD3C,31,35,中国铁路上海局集团有限公司 宁东机务段(上海机务段支配),,
HXD3C,36,41,中国铁路武汉局集团有限公司 江岸机务段(武南机务段支配),,
HXD3C,42,45,中国铁路武汉局集团有限公司 江岸机务段(襄阳机务段支配),,
HXD3C,46,55,中国铁路南昌局集团有限公司 南昌机务段,,
HXD3C,56,60,中国铁路郑州局集团有限公司 郑州机务段,,
HXD3C,61,61,中国铁路沈阳局集团有限公司 沈阳机务段,,
HXD3C,62,62,中国铁路济南局集团有限公司 济南机务段,,
HXD3C,63,63,中国铁路沈阳局集团有限公司 沈阳机务段,,
HXD3C,64,70,中国铁路济南局集团有限公司 济南机务段,,
HXD3C,71,85,中国铁路武汉局集团有限公司 江岸机务段,,
HXD3C,86,95,中国铁路郑州局集团有限公司 郑州机务段,,
HXD3C,96,100,中国铁路济南局集团有限公司 济南机务段,,
HXD3C,101,110,中国铁路武汉局集团有限公司 江岸机务段,,
HXD3C,111,120,中国铁路沈阳局集团有限公司 沈阳机务段,,
HXD3C,121,125,中国铁路上海局集团有限公司 宁东机务段(上海机务段支配),,
HXD3C,126,130,中国铁路南昌局集团有限公司 南昌机务段,,
HXD3C,131,135,中国铁路济南局集团有限公司 济南机务段,,
HXD3C,136,140,中国铁路郑州局集团有限公司 郑州机务段,,
HXD3C,141,165,中国铁路武汉局集团有限公司 江岸机务段,,
HXD3C,166,180,中国铁路成都局集团有限公司 重庆机务段,,
HXD3C,181,182,中国铁路济南局集团有限公司 济南机务段,,
HXD3C,183,190,中国铁路沈阳局集团有限公司 沈阳机务段,,
HXD3C,191,195,中国铁路济南局集团有限公司 济南机务段,,
HXD3C,198,200,中国铁路上海局集团有限公司 宁东机务段,,
HXD3C,201,220,中国铁路武汉局集团有限公司 江岸机务段,,
HXD3C,221,225,中国铁路上海局集团有限公司 宁东机务段,,
HXD3C,226,229,中国铁路沈阳局集团有限公司 沈阳机务段,,
HXD3C,238,238,中国铁路广州局集团有限公司 株洲机务段,,
HXD3C,271,300,中国铁路上海局集团有限公司 宁东机务段,,
HXD3C,446,446,中国铁路广州局集团有限公司 广州机务段,,
HXD3C,805,809,中国铁路广州局集团有限公司 ,,
HXD3C,810,819,中国铁路南宁局集团有限公司,,
HXD3C,820,829,中国铁路武汉局集团有限公司,,
HXD3C,896,925,中国铁路沈阳局集团有限公司,,
HXD3C,926,930,中国铁路南宁局集团有限公司,,
HXD3C,931,945,中国铁路北京局集团有限公司,,
HXD3C,946,955,中国铁路济南局集团有限公司,,
HXD3C,956,965,中国铁路郑州局集团有限公司,,
HXD3C,966,974,中国铁路济南局集团有限公司,,
HXD3D,1,10,中国铁路沈阳局集团有限公司 沈阳机务段,,
HXD3D,11,25,西安铁路局集团有限公司 西安机务段,,
HXD3D,26,34,中国铁路兰州局集团有限公司 兰州西机务段,,
HXD3D,35,35,中国铁路兰州局集团有限公司 迎水桥机务段,雷锋号,
HXD3D,36,38,中国铁路兰州局集团有限公司 兰州西机务段,,
HXD3D,39,39,中国铁路济南局集团有限公司 济南机务段,共青团号,
HXD3D,40,40,中国铁路兰州局集团有限公司 兰州西机务段,,
HXD3D,41,50,西安铁路局集团有限公司 西安机务段,,
HXD3D,51,70,中国铁路北京局集团有限公司 北京机务段,,
HXD3D,71,90,中国铁路南昌局集团有限公司 南昌机务段,,
HXD3D,91,115,中国铁路兰州局集团有限公司 兰州西机务段,,
HXD3D,116,135,中国铁路昆明局集团有限公司 昆明机务段,,
HXD3D,136,145,中国铁路北京局集团有限公司 北京机务段,,
HXD3D,146,150,呼和浩特铁路局集团有限公司 集宁机务段,,
HXD3D,151,155,中国铁路沈阳局集团有限公司 沈阳机务段,,
HXD3D,156,160,中国铁路南昌局集团有限公司 南昌机务段,,
HXD3D,161,165,中国铁路北京局集团有限公司 北京机务段,,
HXD3D,166,170,西安铁路局集团有限公司 西安机务段,,
HXD3D,171,180,中国铁路兰州局集团有限公司 兰州西机务段,,
HXD3D,181,190,中国铁路济南局集团有限公司 济南机务段,,
HXD3D,191,245,中国铁路沈阳局集团有限公司 沈阳机务段,,
HXD3D,246,255,中国铁路北京局集团有限公司 北京机务段,,
HXD3D,256,265,呼和浩特铁路局集团有限公司 集宁机务段,,
HXD3D,266,290,中国铁路兰州局集团有限公司 兰州西机务段,,
HXD3D,291,300,中国铁路沈阳局集团有限公司 沈阳机务段,,
HXD3D,301,310,中国铁路济南局集团有限公司 济南机务段,,
HXD3D,310,315,中国铁路昆明局集团有限公司 昆明机务段,,
HXD3D,316,320,中国铁路南昌局集团有限公司 南昌机务段,,
HXD3D,321,322,呼和浩特铁路局集团有限公司 集宁机务段,,
HXD3D,323,325,中国铁路北京局集团有限公司 北京机务段,,
HXD3D,326,333,西安铁路局集团有限公司 西安机务段,,
HXD3D,334,340,西安铁路局集团有限公司 安康机务段,,
HXD3D,341,345,中国铁路沈阳局集团有限公司 沈阳机务段,,
HXD3D,346,346,中国铁路成都局集团有限公司 重庆机务段,,
HXD3D,351,351,中国铁路成都局集团有限公司 重庆机务段,,
HXD3D,356,365,西安铁路局集团有限公司 安康机务段,,
HXD3D,366,369,中国铁路北京局集团有限公司 北京机务段,,
HXD3D,370,382,呼和浩特铁路局集团有限公司 集宁机务段,,
HXD3D,383,392,中国铁路昆明局集团有限公司 昆明机务段,,
HXD3D,393,397,中国铁路兰州局集团有限公司 兰州西机务段,,
HXD3D,398,402,西安铁路局集团有限公司 西安机务段,,
HXD3D,403,417,西安铁路局集团有限公司 西安机务段,,
HXD3D,418,419,中国铁路哈尔滨局集团有限公司 牡丹江机务段,,
HXD3D,420,424,中国铁路北京局集团有限公司 北京机务段,,
HXD3D,425,429,呼和浩特铁路局集团有限公司 集宁机务段,,
HXD3D,430,433,中国铁路哈尔滨局集团有限公司 牡丹江机务段,,
HXD3D,434,443,中国铁路南昌局集团有限公司 南昌机务段,,
HXD3D,444,449,中国铁路济南局集团有限公司 济南机务段,,
HXD3D,450,464,西安铁路局集团有限公司 西安机务段,,
HXD3D,465,468,中国铁路济南局集团有限公司 济南机务段,,
HXD3D,469,473,中国铁路济南局集团有限公司 济南机务段,,
HXD3D,474,479,中国铁路昆明局集团有限公司 昆明机务段,,
HXD3D,480,484,中国铁路南昌局集团有限公司 南昌机务段,,
HXD3D,485,489,西安铁路局集团有限公司 西安机务段,,
HXD3D,490,499,中国铁路北京局集团有限公司 北京机务段,,
HXD3D,500,503,中国铁路哈尔滨局集团有限公司 牡丹江机务段,,
HXD3D,504,514,中国铁路沈阳局集团有限公司 沈阳机务段,,
HXD3D,515,515,中国铁路成都局集团有限公司 重庆机务段,,
HXD3D,516,518,中国铁路沈阳局集团有限公司 沈阳机务段,,
HXD3D,519,528,西安铁路局集团有限公司 西安机务段,,
HXD3D,529,538,中国铁路北京局集团有限公司 北京机务段,,
HXD3D,539,541,呼和浩特铁路局集团有限公司 集宁机务段,,
HXD3D,542,553,西安铁路局集团有限公司 西安机务段,,
HXD3D,554,563,中国铁路济南局集团有限公司 济南机务段,,
HXD3D,564,568,中国铁路昆明局集团有限公司 昆明机务段,,
HXD3D,569,573,中国铁路成都局集团有限公司 重庆机务段,,
HXD3D,574,583,中国铁路济南局集团有限公司 济南机务段,,
HXD3D,584,584,中国铁路沈阳局集团有限公司 沈阳机务段,,
HXD3D,585,609,中国铁路哈尔滨局集团有限公司 三棵树机务段,,
HXD3D,610,611,中国铁路北京局集团有限公司 北京机务段,,
HXD3D,612,621,中国铁路南昌局集团有限公司 南昌机务段,,
HXD3D,622,626,中国铁路南昌局集团有限公司 南昌机务段,,
HXD3D,627,629,中国铁路哈尔滨局集团有限公司 三棵树机务段,,
HXD3D,630,630,中国铁路哈尔滨局集团有限公司 哈尔滨机务段,,
HXD3D,631,631,西安铁路局集团有限公司 西安机务段,第五代“钢人铁马号”,
HXD3D,632,653,中国铁路沈阳局集团有限公司 沈阳机务段,,
HXD3D,654,673,中国铁路沈阳局集团有限公司 沈阳机务段,,
HXD3D,674,681,中国铁路沈阳局集团有限公司 沈阳机务段,,
HXD3D,682,688,中国铁路兰州局集团有限公司 兰州西机务段,,
HXD3D,1886,1886,中国铁路哈尔滨局集团有限公司 哈尔滨机务段,第五代“朱德号”,
HXD3D,1893,1893,中国铁路北京局集团有限公司 丰台机务段,第六代“毛泽东号”,
HXD3D,1921,1921,中国铁路沈阳局集团有限公司 沈阳机务段,共产党员号,
HXD3D,7001,7002,广西沿海铁路股份有限公司 南宁南机务运用段,,
HXD3D,7003,7003,吉林铁道职业技术学院,,
HXD3D,8001,8025,中国铁路沈阳局集团有限公司 沈阳机务段,,大同
HXD3D,8026,8028,中国铁路太原局集团有限公司 太原南机务段,,大同
东方红2,1,50,,,资阳
东风,1201,1830,,,大连、成都
东风,2001,2094,,,戚墅堰
东风11,1,459,,,戚墅堰
东风12,8001,8001,吉林铁道职业技术学院,,
东风2,3201,3348,,,戚墅堰
东风21,1,5,中国铁路昆明局集团有限公司 昆明机务段,,
东风21,6,6,中国铁路昆明局集团有限公司 昆明机务段,状元号,
东风21,7,7,中国铁路昆明局集团有限公司 昆明机务段,亲年号,
东风21,8,8,中国铁路昆明局集团有限公司 昆明机务段,建水古城,
东风21,9,100,中国铁路昆明局集团有限公司 昆明机务段,,
东风21,101,101,中国铁路昆明局集团有限公司 昆明机务段,异龙号,
东风21,102,102,中国铁路昆明局集团有限公司 昆明机务段,,
东风21,1001,1002,云南钢铁厂,,
东风2Z,3251,3251,*齐齐哈尔铁路局 加格达奇机务段,,
东风3,3243,3243,中车共享城机车公园,,
东风4,3247,3247,中车成都轨道交通产业园,,
东风4B,1001,1999,,,大连
东风4B,1963,1963,*北京铁路局 丰台机务段,,
东风4B,2101,2685,,,大连
东风4B,2104,2104,*上海铁路局 蚌埠机务段,,
东风4B,2376,2376,*南昌铁路局 鹰潭机务段,,
东风4B,3101,3999,,,资阳
东风4B,3214,3214,*浙江金温铁道开发有限公司 温州机务段,,
东风4B,3249,3249,*西安铁路局 西安机务段,,
东风4B,3390,3390,*成都铁路局 重庆机务段,,
东风4B,3593,3593,*中国铁路广州局集团有限公司 株洲机务段,,
东风4B,6001,6587,,,大同
东风4B,6530,6530,*南宁铁路局 南宁机务段,,
东风4B,7001,7363,,,大连
东风4B,7364,7365,,,四方
东风4B,7366,7796,,,大连
东风4B,7701,7732,,,戚墅堰改
东风4B,9001,9702,,,资阳
东风4B,9167,9167,*南昌铁路局 向塘机务段,,
东风4B,9531,9531,*新长铁路公司,,
东风4C,1,10,,,大同
东风4C,11,11,中国铁路北京局集团有限公司 丰台段,青年文明号,
东风4C,12,40,,,大同
东风4C,2001,2006,,,四方
东风4C,4001,4465,,,大连
东风4C,4466,4466,四方机车车辆厂,,四方
东风4C,5001,5273,,,资阳
东风4C,5274,5275,三茂铁路公司 三水机务段,东风4CK,
东风4C,5276,5335,,,资阳
东风4D,7001,7021,中国铁路南宁局集团有限公司,,
东风5,1,1,中国铁路北京局集团有限公司 北京车辆段,,
东风5,1974,1975,中国铁路兰州局集团有限公司 兰州西机务段,,唐山
东风5,1976,2082,,,唐山
东风5,2083,2083,中国石油兰州石化公司,,唐山
东风5,3279,3279,云南铁路博物馆,,
东风6,1,2,*沈阳铁路局 大连机务段,,
东风6,3,3,沈阳铁路陈列馆,,
东风6,4,4,*沈阳铁路局 大连机务段,,
东风7,174,174,中国铁路太原局集团有限公司 太原机务段北场,,
东风7B,3006,3006,中国铁道博物馆,,
东风7B,3015,3015,王坪村铁路公园,,
东风7B,6001,6072,*北京铁路局 邯郸机务段;郑州铁路局 新乡机务段,调车,
东风7D,1,1,中国铁道博物馆,,
东风7D,3001,3001,中国铁道博物馆,,
东风7E,1,1,中国铁路郑州局集团有限公司 新乡机务段,,
东风7E,2,2,中国铁路郑州局集团有限公司 郑州机务段,,
东风7G,9001,9004,呼和浩特铁路局 集宁机务段 赛汗塔拉分段,,
东风8,1,1,中国铁道博物馆,,
东风9,1,2,中国铁路广州局集团有限公司广州机务段,,
韶山1,8,8,中国铁道博物馆,,
韶山1,156,156,郑州世纪欢乐园,,
韶山1,160,160,北京铁路电气化学校,,
韶山1,227,227,中国铁路兰州局集团有限公司 兰州西机务段,,
韶山1,254,254,中国铁路北京局集团有限公司 丰台机务段 储备厂,,
韶山1,307,307,中国铁路太原局集团有限公司 榆次机务折返段,,
韶山1,309,309,中国铁路太原局集团有限公司 太原机务段北场,,
韶山1,321,321,武汉铁路职业技术学院,,
韶山1,681,681,中国铁道博物馆,,
韶山1,695,695,沈阳铁路陈列馆,,
韶山1,762,762,中国铁路广州局集团有限公司 娄底运用车间储备厂,,
韶山1,818,818,西南交通大学 机车博物园,,
韶山1,821,821,韶关机务实训基地,,
韶山1,826,826,韶关机务实训基地,,
韶山3,454,454,中国铁路成都局集团有限公司 贵阳机务段,先锋号,
韶山3,524,524,中国铁路武汉局集团有限公司 江岸机务段,青年号,
韶山3,4160,4160,广西沿海铁路公司 南宁南机务运用段,共青团号,
韶山3,4178,4178,广西沿海铁路公司 南宁南机务运用段,共青团号,
韶山3,4235,4235,中国铁路成都局集团有限公司 重庆机务段,青年文明号,
韶山3,4258,4258,中国铁路成都局集团有限公司 重庆机务段,党员先锋号,
韶山3,5080,5080,广州铁路博物馆,,
韶山3,6005,6005,湖南交通工程学院,,
韶山3,8050,8050,武汉四美塘铁路遗址公园,,
韶山3B,16,16,西安铁路局 安康机务段,青年文明号,
韶山3B,5001,5001,中国铁路成都局集团有限公司 贵阳机务段,*先锋力神,
韶山3B,5035,5035,中国铁路兰州局集团有限公司 迎水桥机务段,雷锋号 (曾),
韶山3B,5038,5038,中国铁路兰州局集团有限公司 迎水桥机务段,青年文明号,
韶山3B,5151,5151,中国铁路成都局集团有限公司 西昌机务段,扶贫先锋号,
韶山3B,5162,5162,中国铁路昆明局集团有限公司 昆明机务段,五四青年号,
韶山3B,5235,5235,中国铁路成都局集团有限公司 西昌机务段,*共青团号,
韶山3C,1,1,中国铁路成都局集团有限公司 贵阳机务段,,
韶山4,6,6,中国铁道博物馆,,
韶山4,10,10,中国铁路成都局集团有限公司 西昌机务段,,
韶山4,50,50,中国铁路郑州局集团有限公司 新乡机务段,先锋号,
韶山4,63,63,中国铁路太原局集团有限公司 太原机务段,,
韶山4,204,204,中国铁路郑州局集团有限公司 新乡机务段,先锋号,
韶山4,448,448,中国铁路沈阳局集团有限公司 苏家屯机务段,先锋号,
韶山4,574,574,中铁三局集团,先锋号,
韶山4,743,743,中国铁路哈尔滨局集团有限公司 哈尔滨机务段,青年文明号,
韶山4,855,855,西安铁路局 新丰镇机务段,,
韶山4,911,911,中铁三局集团,青年文明号,
韶山4,2006,2006,吉林铁道职业技术学院,,
韶山4B,19,19,神朔铁路公司 神木北机务段,青年号,
韶山4B,89,89,神朔铁路公司 神木北机务段,青年文明号,
韶山4B,90,90,神朔铁路公司 神木北机务段,青年文明号,
韶山4B,257,257,包神铁路公司 东胜机务段,党员先锋号,
韶山4G,159,1177,,,株洲
韶山4G,168,168,中国铁道博物馆,,
韶山4G,171,171,中国铁路哈尔滨局集团有限公司 牡丹江机务段,,
韶山4G,179,179,中国铁路太原局集团有限公司 湖东机务段,,
韶山4G,466,466,石家庄铁道大学,,
韶山4G,1089,1089,*呼和浩特铁路局 包头西机务段,,
韶山4G,1886,1886,中国铁路哈尔滨局集团有限公司 哈尔滨机务段,*朱德号,株洲
韶山4G,3001,3002,,,资阳
韶山4G,6001,6001,中国铁道博物馆,,
韶山4G,6001,6001,中国铁道博物馆,,大同
韶山4G,7001,7110,,,大连
韶山4G,7121,7243,,,大连
韶山5,1,1,中国铁道博物馆,,
韶山5,2,2,郑州世纪欢乐园 ,,
韶山6,1,1,郑州铁路司机学校,,
韶山6,2,2,中国铁道博物馆,,
韶山6B,1011,1011,西安铁路局 西安机务段,*青年文明号,
韶山6B,1026,1026,韶关机务实训基地,,
韶山6B,1088,1088,中国铁路武汉局集团有限公司 襄阳机务段,*民兵号,
韶山6B,1111,1111,中国铁路武汉局集团有限公司 襄阳机务段,*先锋号,
韶山6B,6001,6001,韶关机务实训基地,,
韶山6B,6002,6002,广州铁路博物馆,,
韶山7,1,79,中国铁路南宁局集团有限公司 柳州机务段,,
韶山7,76,76,中国铁路南宁局集团有限公司 南宁机务段,*五四红旗号,
韶山7,80,84,中国铁路南宁局集团有限公司 柳州机务段,,
韶山7,85,111,中国铁路南宁局集团有限公司 柳州机务段,,
韶山7,8112,8113,山西孝柳铁路有限责任公司,,
韶山7B,1,1,*南宁铁路局集团有限公司 南宁机务段,,
韶山7B,2,2,中国铁路南宁局集团有限公司 柳州机务段,,
韶山7D,1,58,西安铁路局集团有限公司 西安机务段,,
韶山7D,631,631,西安铁路局集团有限公司 西安机务段,*钢人铁马号,
韶山7E,1,140,,,大同
韶山7E,6001,6002,中国铁路昆明局集团有限公司,,大同
韶山7E,7001,7004,,,大连
韶山8,1,1,中国铁路广州局集团有限公司 广州机务段,,
韶山8,2,2,中国铁路广州局集团有限公司 广州机务段,,
韶山8,3,4,中国铁路上海局集团有限公司 上海机务段,,
韶山8,5,5,中国铁路郑州局集团有限公司 郑州机务段,,
韶山8,9,9,中国铁路郑州局集团有限公司 郑州机务段,,
韶山8,11,11,中国铁路郑州局集团有限公司 郑州机务段,,
韶山8,12,12,中国铁路郑州局集团有限公司 郑州机务段,,
韶山8,15,16,中国铁路郑州局集团有限公司 郑州机务段,,
韶山8,17,17,中国铁路上海局集团有限公司 上海机务段,,
韶山8,20,20,中国铁路郑州局集团有限公司 郑州机务段,,
韶山8,24,25,中国铁路郑州局集团有限公司 郑州机务段,,
韶山8,27,27,中国铁路郑州局集团有限公司 郑州机务段,,
韶山8,29,32,中国铁路郑州局集团有限公司 郑州机务段,,
韶山8,33,35,中国铁路上海局集团有限公司 上海机务段,,
韶山8,36,36,中国铁路郑州局集团有限公司 郑州机务段,,
韶山8,38,38,中国铁路上海局集团有限公司 上海机务段,,
韶山8,39,39,中国铁路上海局集团有限公司 上海机务段,国祥号,
韶山8,40,40,中国铁路上海局集团有限公司 上海机务段,,
韶山8,41,41,中国铁路北京局集团有限公司 北京机务段,,
韶山8,43,43,中国铁路郑州局集团有限公司 郑州机务段,,
韶山8,44,44,中国铁路北京局集团有限公司 邯郸机务段,,
韶山8,45,45,中国铁路郑州局集团有限公司 郑州机务段,,
韶山8,48,48,中国铁路北京局集团有限公司 邯郸机务段,,
韶山8,49,49,中国铁路南昌局集团有限公司 南昌机务段,,
韶山8,50,50,中国铁路南昌局集团有限公司 南昌机务段,,
韶山8,51,51,中国铁路北京局集团有限公司 邯郸机务段,,
韶山8,52,52,中国铁路上海局集团有限公司 上海机务段,,
韶山8,55,55,中国铁路南昌局集团有限公司 南昌机务段,,
韶山8,56,57,中国铁路北京局集团有限公司 邯郸机务段,,
韶山8,64,64,中国铁路广州局集团有限公司 广州机务段,,
韶山8,72,72,中国铁路北京局集团有限公司 邯郸机务段,,
韶山8,73,73,中国铁路北京局集团有限公司 北京机务段,,
韶山8,74,74,中国铁路北京局集团有限公司 邯郸机务段,,
韶山8,81,81,中国铁路北京局集团有限公司 北京机务段,,
韶山8,83,84,中国铁路郑州局集团有限公司 郑州机务段,,
韶山8,85,85,中国铁路北京局集团有限公司 北京机务段,,
韶山8,88,103,中国铁路郑州局集团有限公司 郑州机务段,,
韶山8,104,104,中国铁路北京局集团有限公司 邯郸机务段,,
韶山8,109,111,中国铁路南昌局集团有限公司 南昌机务段,,
韶山8,114,116,中国铁路南昌局集团有限公司 南昌机务段,,
韶山8,118,119,中国铁路北京局集团有限公司 北京机务段,,
韶山8,121,126,中国铁路北京局集团有限公司 北京机务段,,
韶山8,127,128,中国铁路郑州局集团有限公司 郑州机务段,,
韶山8,130,130,中国铁路南昌局集团有限公司 南昌机务段,,
韶山8,131,131,中国铁路广州局集团有限公司 长沙机务段,,
韶山8,132,132,中国铁路广州局集团有限公司 长沙机务段,,
韶山8,133,133,中国铁路广州局集团有限公司 长沙机务段,,
韶山8,134,134,中国铁路广州局集团有限公司 长沙机务段,,
韶山8,136,136,中国铁路广州局集团有限公司 长沙机务段,,
韶山8,141,141,中国铁路广州局集团有限公司 广州机务段,,
韶山8,144,144,中国铁路广州局集团有限公司 长沙机务段,,
韶山8,148,148,中国铁路广州局集团有限公司 广州机务段,,
韶山8,156,156,中国铁路广州局集团有限公司 广州机务段,,
韶山8,163,163,中国铁路广州局集团有限公司 广州机务段,,
韶山8,166,166,中国铁路广州局集团有限公司 广州机务段,新世纪金龙号,
韶山8,171,171,中国铁路上海局集团有限公司 上海机务段,,
韶山8,172,172,中国铁路郑州局集团有限公司 郑州机务段,,
韶山8,173,173,中国铁路广州局集团有限公司 广州机务段,,
韶山8,181,181,中国铁路广州局集团有限公司 广州机务段,,
韶山8,186,186,中国铁路广州局集团有限公司 广州机务段,,
韶山8,191,191,中国铁路广州局集团有限公司 广州机务段,,
韶山8,192,192,中国铁路广州局集团有限公司 广州机务段,,
韶山8,197,197,中国铁路郑州局集团有限公司 郑州机务段,,
韶山8,200,204,中国铁路上海局集团有限公司 上海机务段,,
韶山8,205,205,中国铁路广州局集团有限公司 长沙机务段,,
韶山8,214,214,中国铁路郑州局集团有限公司 郑州机务段,,
韶山9,1,3,中国铁路沈阳局集团有限公司 沈阳机务段;中国铁路上海局集团有限公司 上海机务段,,
韶山9,5,29,中国铁路沈阳局集团有限公司 沈阳机务段;中国铁路上海局集团有限公司 上海机务段,,
韶山9,30,30,中国铁路沈阳局集团有限公司 通辽机务段,,
韶山9,31,37,中国铁路沈阳局集团有限公司 沈阳机务段;中国铁路上海局集团有限公司 上海机务段,,
韶山9,38,38,中国铁路沈阳局集团有限公司 通辽机务段,,
韶山9,39,43,中国铁路沈阳局集团有限公司 沈阳机务段;中国铁路上海局集团有限公司 上海机务段,,
1 6G 51 90 西安铁路局 宝鸡电力机务段
2 6K 1 85 中国铁路郑州局集团有限公司 洛阳机务段
3 8G 1 1 中国铁路太原局集团有限公司 太原北机务段、侯马机务段、石家庄电力机务段
4 8G 2 2 中国铁道博物馆
5 8G 3 75 中国铁路太原局集团有限公司 太原北机务段、侯马机务段、石家庄电力机务段
6 8G 76 76 中国铁路太原局集团有限公司 太原机务段北场
7 8G 77 96 中国铁路太原局集团有限公司 太原北机务段、侯马机务段、石家庄电力机务段
8 8G 97 97 中国铁路太原局集团有限公司 榆次机务折返段
9 8G 98 100 中国铁路太原局集团有限公司 太原北机务段、侯马机务段、石家庄电力机务段
10 8K 1 1 中国铁路北京局集团有限公司 丰台机务段
11 8K 2 7 *北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段
12 8K 8 8 中国铁道博物馆 *科技号
13 8K 9 17 *北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段
14 8K 18 18 *北京铁路局 丰台机务段
15 8K 19 23 *北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段
16 8K 24 24 中国铁路太原局集团有限公司 湖东机务段 大同西运用车间
17 8K 25 64 *北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段
18 8K 65 65 天津铁道职业技术学院
19 8K 66 71 *北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段
20 8K 72 72 中国铁路北京局集团有限公司 丰台机务段
21 8K 73 90 *北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段
22 8K 91 91 中国铁路太原局集团有限公司 太原机务段北场 机车展场
23 8K 92 100 *北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段
24 CR400AF 21 21 中国铁路北京局集团有限公司 朝阳动车运用所 CR400AF-G
25 CR400AF 207 208 中国铁路北京局集团有限公司 北京西动车运用所
26 CR400AF 1001 1002 中国铁路广州局集团有限公司 深圳动车运用所 CR400AF-A
27 CR400AF 1003 1003 中国铁路广州局集团有限公司 广州南动车运用所 CR400AF-A
28 CR400AF 1004 1004 中国铁路广州局集团有限公司 深圳动车运用所 CR400AF-A
29 CR400AF 1005 1005 中国铁路广州局集团有限公司 广州南动车运用所 CR400AF-A
30 CR400AF 1006 1006 中国铁路广州局集团有限公司 广州南动车运用所
31 CR400AF 1007 1009 中国铁路广州局集团有限公司 潮州动车运用所
32 CR400AF 1010 1010 中国铁路广州局集团有限公司 广州南动车运用所
33 CR400AF 1011 1014 中国铁路广州局集团有限公司 潮州动车运用所
34 CR400AF 1015 1020 中国铁路广州局集团有限公司 广州南动车运用所
35 CR400AF 1021 1021 中国铁路广州局集团有限公司 潮州动车运用所
36 CR400AF 1022 1025 中国铁路广州局集团有限公司 广州南动车运用所
37 CR400AF 1026 1027 中国铁路广州局集团有限公司 广州南动车运用所 CR400AF-A
38 CR400AF 1028 1029 中国铁路广州局集团有限公司 深圳动车运用所 CR400AF-A
39 CR400AF 1030 1030 中国铁路广州局集团有限公司 广州南动车运用所 CR400AF-A
40 CR400AF 1031 1031 中国铁路广州局集团有限公司 深圳动车运用所 CR400AF-A
41 CR400AF 1032 1032 中国铁路广州局集团有限公司 广州南动车运用所 CR400AF-A
42 CR400AF 1033 1033 中国铁路广州局集团有限公司 深圳动车运用所 CR400AF-A
43 CR400AF 1034 1038 中国铁路广州局集团有限公司 广州南动车运用所 CR400AF-A
44 CR400AF 1039 1039 中国铁路广州局集团有限公司 潮州动车运用所
45 CR400AF 1040 1040 中国铁路广州局集团有限公司 广州南动车运用所
46 CR400AF 2002 2002 中国铁路北京局集团有限公司 北京西动车运用所
47 CR400AF 2004 2004 中国铁路北京局集团有限公司 北京西动车运用所
48 CR400AF 2005 2005 中国铁路成都局集团有限公司 重庆西动车运用所
49 CR400AF 2006 2007 中国铁路北京局集团有限公司 北京西动车运用所
50 CR400AF 2008 2008 中国铁路北京局集团有限公司 雄安动车运用所
51 CR400AF 2009 2010 中国铁路北京局集团有限公司 北京西动车运用所
52 CR400AF 2011 2011 中国铁路广州局集团有限公司 长沙动车运用所
53 CR400AF 2012 2012 中国铁路北京局集团有限公司 北京西动车运用所
54 CR400AF 2013 2013 中国铁路成都局集团有限公司 重庆西动车运用所
55 CR400AF 2014 2016 中国铁路北京局集团有限公司 北京西动车运用所
56 CR400AF 2017 2017 中国铁路广州局集团有限公司 长沙动车运用所
57 CR400AF 2023 2023 中国铁路北京局集团有限公司 北京西动车运用所
58 CR400AF 2024 2024 中国铁路广州局集团有限公司 长沙动车运用所
59 CR400AF 2025 2025 中国铁路成都局集团有限公司 重庆西动车运用所
60 CR400AF 2026 2028 中国铁路广州局集团有限公司 长沙动车运用所
61 CR400AF 2030 2030 中国铁路北京局集团有限公司 雄安动车运用所
62 CR400AF 2031 2032 中国铁路成都局集团有限公司 重庆西动车运用所
63 CR400AF 2033 2033 中国铁路北京局集团有限公司 雄安动车运用所
64 CR400AF 2034 2034 中国铁路北京局集团有限公司 北京西动车运用所
65 CR400AF 2035 2038 中国铁路广州局集团有限公司 长沙动车运用所
66 CR400AF 2040 2046 中国铁路广州局集团有限公司 长沙动车运用所
67 CR400AF 2047 2048 中国铁路北京局集团有限公司 北京西动车运用所
68 CR400AF 2049 2049 中国铁路成都局集团有限公司 重庆西动车运用所
69 CR400AF 2051 2051 中国铁路广州局集团有限公司 长沙动车运用所
70 CR400AF 2053 2055 中国铁路广州局集团有限公司 长沙动车运用所
71 CR400AF 2057 2057 中国铁路广州局集团有限公司 长沙动车运用所
72 CR400AF 2058 2058 中国铁路北京局集团有限公司 雄安动车运用所
73 CR400AF 2060 2062 中国铁路广州局集团有限公司 长沙动车运用所
74 CR400AF 2064 2064 中国铁路广州局集团有限公司 长沙动车运用所
75 CR400AF 2065 2066 中国铁路广州局集团有限公司 深圳动车运用所 CR400AF-A
76 CR400AF 2067 2068 中国铁路广州局集团有限公司 长沙动车运用所 CR400AF-A
77 CR400AF 2069 2069 中国铁路广州局集团有限公司 深圳动车运用所 CR400AF-A
78 CR400AF 2070 2070 中国铁路广州局集团有限公司 长沙动车运用所 CR400AF-A
79 CR400AF 2071 2071 中国铁路广州局集团有限公司 深圳动车运用所 CR400AF-A
80 CR400AF 2072 2072 中国铁路广州局集团有限公司 长沙动车运用所 CR400AF-A
81 CR400AF 2073 2073 中国铁路广州局集团有限公司 深圳动车运用所 CR400AF-A
82 CR400AF 2074 2076 中国铁路广州局集团有限公司 长沙动车运用所 CR400AF-A
83 CR400AF 2077 2079 中国铁路广州局集团有限公司 深圳动车运用所 CR400AF-A
84 CR400AF 2080 2084 中国铁路广州局集团有限公司 长沙动车运用所 CR400AF-A
85 CR400AF 2085 2085 中国铁路济南局集团有限公司 济南东动车运用所
86 CR400AF 2086 2086 中国铁路济南局集团有限公司 青岛动车运用所
87 CR400AF 2087 2087 中国铁路济南局集团有限公司 济南东动车运用所
88 CR400AF 2088 2090 中国铁路济南局集团有限公司 青岛动车运用所
89 CR400AF 2091 2094 中国铁路济南局集团有限公司 济南东动车运用所
90 CR400AF 2095 2097 中国铁路广州局集团有限公司 长沙动车运用所 CR400AF-A
91 CR400AF 2098 2098 中国铁路广州局集团有限公司 深圳动车运用所 CR400AF-A
92 CR400AF 2099 2100 中国铁路广州局集团有限公司 长沙动车运用所 CR400AF-A
93 CR400AF 2102 2102 中国铁路济南局集团有限公司 济南东动车运用所 CR400AF-A
94 CR400AF 2103 2104 中国铁路广州局集团有限公司 长沙动车运用所 CR400AF-A
95 CR400AF 2105 2105 中国铁路广州局集团有限公司 深圳动车运用所 CR400AF-A
96 CR400AF 2106 2106 中国铁路广州局集团有限公司 长沙动车运用所 CR400AF-A
97 CR400AF 2107 2115 中国铁路济南局集团有限公司 济南东动车运用所 CR400AF-A
98 CR400AF 2116 2123 中国铁路北京局集团有限公司 北京南动车运用所 CR400AF-B
99 CR400AF 2124 2124 中国铁路武汉局集团有限公司 武汉动车运用所
100 CR400AF 2125 2125 中国铁路武汉局集团有限公司 武汉动车运用所
101 CR400AF 2126 2127 中国铁路武汉局集团有限公司 汉口动车运用所
102 CR400AF 2128 2128 中国铁路武汉局集团有限公司 武汉动车运用所
103 CR400AF 2130 2131 中国铁路广州局集团有限公司 长沙动车运用所
104 CR400AF 2133 2133 中国铁路广州局集团有限公司 长沙动车运用所
105 CR400AF 2134 2134 中国铁路济南局集团有限公司 青岛动车运用所
106 CR400AF 2135 2135 中国铁路成都局集团有限公司 重庆西动车运用所
107 CR400AF 2136 2138 中国铁路济南局集团有限公司 青岛动车运用所
108 CR400AF 2139 2139 中国铁路济南局集团有限公司 济南东动车运用所
109 CR400AF 2140 2140 中国铁路成都局集团有限公司 重庆西动车运用所
110 CR400AF 2141 2141 中国铁路济南局集团有限公司 济南东动车运用所
111 CR400AF 2142 2144 中国铁路北京局集团有限公司 北京西动车运用所
112 CR400AF 2145 2146 中国铁路北京局集团有限公司 雄安动车运用所
113 CR400AF 2148 2150 中国铁路武汉局集团有限公司 汉口动车运用所
114 CR400AF 2151 2151 中国铁路武汉局集团有限公司 武汉动车运用所
115 CR400AF 2152 2153 中国铁路武汉局集团有限公司 汉口动车运用所
116 CR400AF 2154 2156 中国铁路武汉局集团有限公司 武汉动车运用所
117 CR400AF 2159 2159 中国铁路武汉局集团有限公司 武汉动车运用所
118 CR400AF 2160 2161 中国铁路武汉局集团有限公司 汉口动车运用所
119 CR400AF 2162 2163 中国铁路济南局集团有限公司 济南东动车运用所
120 CR400AF 2164 2164 中国铁路武汉局集团有限公司 武汉动车运用所
121 CR400AF 2171 2172 中国铁路武汉局集团有限公司 武汉动车运用所
122 CR400AF 2173 2173 中国铁路武汉局集团有限公司 汉口动车运用所
123 CR400AF 2174 2177 中国铁路武汉局集团有限公司 武汉动车运用所
124 CR400AF 2178 2178 中国铁路北京局集团有限公司 雄安动车运用所
125 CR400AF 2179 2179 中国铁路北京局集团有限公司 北京西动车运用所
126 CR400AF 2180 2180 中国铁路北京局集团有限公司 雄安动车运用所
127 CR400AF 2181 2182 中国铁路北京局集团有限公司 北京西动车运用所
128 CR400AF 2183 2187 中国铁路北京局集团有限公司 北京南动车运用所
129 CR400AF 2190 2192 中国铁路武汉局集团有限公司 武汉动车运用所 CR400AF-A
130 CR400AF 2193 2193 中国铁路济南局集团有限公司 济南东动车运用所 CR400AF-A
131 CR400AF 2194 2195 中国铁路广州局集团有限公司 深圳动车运用所 CR400AF-A
132 CR400AF 2196 2200 中国铁路武汉局集团有限公司 武汉动车运用所 CR400AF-A
133 CR400AF 2201 2205 中国铁路济南局集团有限公司 济南东动车运用所 CR400AF-A
134 CR400AF 2206 2210 中国铁路北京局集团有限公司 北京南动车运用所 CR400AF-B
135 CR400AF 2211 2212 中国铁路济南局集团有限公司 济南东动车运用所 CR400AF-A
136 CR400AF 2213 2213 中国铁路北京局集团有限公司 北京西动车运用所
137 CR400AF 2215 2217 中国铁路北京局集团有限公司 朝阳动车运用所 CR400AF-G
138 CR400AF 2222 2225 中国铁路上海局集团有限公司 上海南动车运用所
139 CR400AF 2226 2226 中国铁路广州局集团有限公司 广州南动车运用所
140 CR400AF 2227 2227 中国铁路广州局集团有限公司 长沙动车运用所
141 CR400AF 2228 2228 中国铁路广州局集团有限公司 广州南动车运用所
142 CR400AF 2229 2229 中国铁路广州局集团有限公司 长沙动车运用所
143 CR400AF 2230 2231 中国铁路济南局集团有限公司 济南东动车运用所
144 CR400AF 2232 2235 中国铁路上海局集团有限公司 上海南动车运用所
145 CR400AF 2236 2236 中国铁路成都局集团有限公司 重庆西动车运用所
146 CR400AF 2237 2243 中国铁路上海局集团有限公司 上海南动车运用所
147 CR400AF 2244 2248 中国铁路成都局集团有限公司 重庆西动车运用所
148 CR400AF 2254 2256 中国铁路成都局集团有限公司 重庆西动车运用所
149 DJ1 1 1 中国铁道科学研究院 环形铁道
150 DJ1 2 2 株洲西门子牵引设备有限公司
151 DJ1 3 3 西安铁路局 宝鸡机务段 秦岭附加队
152 DJ2 1 1 中国铁路郑州局集团有限公司 郑州机务段京武快车队 奥星
153 DJ2 2 3 中国铁路郑州局集团有限公司 郑州机务段 奥星
154 HXD1D 1 15 中国铁路武汉局集团有限公司 武昌南机务段
155 HXD1D 16 16 中国铁路上海局集团有限公司 杭州机务段
156 HXD1D 17 17 中国铁路南昌局集团有限公司 南昌机务段
157 HXD1D 18 18 中国铁路南昌局集团有限公司 鹰潭机务段
158 HXD1D 19 19 中国铁路上海局集团有限公司 杭州机务段
159 HXD1D 20 20 中国铁路南昌局集团有限公司 南昌机务段
160 HXD1D 21 21 中国铁路上海局集团有限公司 杭州机务段
161 HXD1D 22 24 中国铁路南昌局集团有限公司 南昌机务段
162 HXD1D 25 25 中国铁路南昌局集团有限公司 鹰潭机务段
163 HXD1D 26 26 中国铁路南昌局集团有限公司 南昌机务段
164 HXD1D 27 27 中国铁路上海局集团有限公司 杭州机务段
165 HXD1D 28 28 中国铁路南昌局集团有限公司 鹰潭机务段
166 HXD1D 29 34 中国铁路南昌局集团有限公司 南昌机务段
167 HXD1D 35 35 中国铁路上海局集团有限公司 杭州机务段
168 HXD1D 36 38 中国铁路兰州局集团有限公司 兰州西机务段
169 HXD1D 39 39 中国铁路济南局集团有限公司 济南机务段
170 HXD1D 40 50 中国铁路兰州局集团有限公司 兰州西机务段
171 HXD1D 51 75 中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段
172 HXD1D 76 105 中国铁路兰州局集团有限公司 兰州西机务段
173 HXD1D 106 137 中国铁路上海局集团有限公司 上海机务段
174 HXD1D 138 168 中国铁路上海局集团有限公司 杭州机务段
175 HXD1D 169 175 中国铁路上海局集团有限公司 上海机务段
176 HXD1D 176 185 中国铁路武汉局集团有限公司 武昌南机务段
177 HXD1D 186 187 中国铁路南昌局集团有限公司 鹰潭机务段
178 HXD1D 188 188 中国铁路南昌局集团有限公司 南昌机务段
179 HXD1D 189 190 中国铁路南昌局集团有限公司 鹰潭机务段
180 HXD1D 191 232 中国铁路南昌局集团有限公司 南昌机务段
181 HXD1D 233 233 中国铁路南昌局集团有限公司 鹰潭机务段
182 HXD1D 234 237 中国铁路南昌局集团有限公司 南昌机务段
183 HXD1D 238 257 中国铁路广州局集团有限公司 广州机务段
184 HXD1D 258 270 中国铁路武汉局集团有限公司 武昌南机务段
185 HXD1D 271 275 中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段
186 HXD1D 276 279 中国铁路上海局集团有限公司 上海机务段
187 HXD1D 280 289 中国铁路上海局集团有限公司 徐州机务段
188 HXD1D 290 291 中国铁路南昌局集团有限公司 南昌机务段
189 HXD1D 292 293 中国铁路南昌局集团有限公司 鹰潭机务段
190 HXD1D 294 295 中国铁路南昌局集团有限公司 南昌机务段
191 HXD1D 296 300 中国铁路广州局集团有限公司 广州机务段
192 HXD1D 301 310 中国铁路武汉局集团有限公司 武昌南机务段
193 HXD1D 311 320 中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段
194 HXD1D 321 340 中国铁路青藏集团有限公司 西宁机务段
195 HXD1D 341 362 中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段
196 HXD1D 363 382 中国铁路广州局集团有限公司 广州机务段
197 HXD1D 383 392 中国铁路南昌局集团有限公司 南昌机务段
198 HXD1D 393 405 中国铁路上海局集团有限公司 上海机务段
199 HXD1D 406 415 中国铁路兰州局集团有限公司 兰州西机务段
200 HXD1D 416 430 中国铁路广州局集团有限公司 长沙机务段
201 HXD1D 431 440 中国铁路南昌局集团有限公司 南昌机务段
202 HXD1D 441 445 中国铁路南昌局集团有限公司 南昌机务段
203 HXD1D 446 450 中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段
204 HXD1D 451 460 中国铁路武汉局集团有限公司 武昌南机务段
205 HXD1D 461 470 中国铁路广州局集团有限公司 广州机务段
206 HXD1D 471 478 中国铁路兰州局集团有限公司 兰州西机务段
207 HXD1D 479 483 中国铁路上海局集团有限公司 上海机务段
208 HXD1D 484 488 中国铁路上海局集团有限公司 杭州机务段
209 HXD1D 489 490 中国铁路上海局集团有限公司 杭州机务段
210 HXD1D 491 510 中国铁路郑州局集团有限公司 郑州机务段
211 HXD1D 511 512 中国铁路上海局集团有限公司 徐州机务段
212 HXD1D 513 515 中国铁路上海局集团有限公司 上海机务段
213 HXD1D 516 520 中国铁路南昌局集团有限公司 南昌机务段
214 HXD1D 521 534 中国铁路广州局集团有限公司 广州机务段
215 HXD1D 522 522 广州铁路职业技术学院
216 HXD1D 535 544 中国铁路南昌局集团有限公司 南昌机务段
217 HXD1D 545 551 中国铁路上海局集团有限公司 上海机务段
218 HXD1D 552 554 中国铁路上海局集团有限公司 杭州机务段
219 HXD1D 555 559 中国铁路郑州局集团有限公司 郑州机务段
220 HXD1D 560 564 中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段
221 HXD1D 565 570 中国铁路广州局集团有限公司 广州机务段
222 HXD1D 571 585 中国铁路南昌局集团有限公司 南昌机务段
223 HXD1D 586 595 中国铁路上海局集团有限公司 杭州机务段
224 HXD1D 596 613 中国铁路兰州局集团有限公司 兰州西机务段
225 HXD1D 614 623 中国铁路广州局集团有限公司 广州机务段
226 HXD1D 624 633 中国铁路上海局集团有限公司 上海机务段
227 HXD1D 634 636 中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段
228 HXD1D 637 644 中国铁路郑州局集团有限公司 郑州机务段
229 HXD1D 645 660 中国铁路郑州局集团有限公司 郑州机务段
230 HXD1D 661 668 中国铁路武汉局集团有限公司 武昌南机务段
231 HXD1D 669 673 中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段
232 HXD1D 674 678 中国铁路郑州局集团有限公司 郑州机务段
233 HXD1D 679 682 中国铁路上海局集团有限公司 上海机务段
234 HXD1D 683 683 中国铁路上海局集团有限公司 徐州机务段
235 HXD1D 684 684 中国铁路上海局集团有限公司 徐州机务段
236 HXD1D 685 689 中国铁路青藏集团有限公司 格尔木机务段
237 HXD1D 1898 1898 中国铁路上海局集团有限公司 上海机务段 周恩来号
238 HXD1D-J 1 3 中国铁路青藏集团有限公司 拉萨动车运用所
239 HXD1D-J 1001 1009 中国铁路昆明局集团有限公司 昆明动车运用所
240 HXD1D-J 1010 1013 中国铁路青藏集团有限公司 格尔木机务段
241 HXD1D-J 1014 1019 中国铁路成都局集团有限公司 成都动车运用所
242 HXD1D-J 1020 1027 中国铁路昆明局集团有限公司 昆明动车运用所
243 HXD3C 1 9 中国铁路沈阳局集团有限公司 沈阳机务段
244 HXD3C 10 10 中国铁路沈阳局集团有限公司 沈阳机务段
245 HXD3C 11 15 中国铁路济南局集团有限公司 济南机务段
246 HXD3C 16 20 中国铁路武汉局集团有限公司 江岸机务段(襄阳机务段支配)
247 HXD3C 21 25 中国铁路南昌局集团有限公司 南昌机务段
248 HXD3C 26 30 中国铁路郑州局集团有限公司 郑州机务段
249 HXD3C 31 35 中国铁路上海局集团有限公司 宁东机务段(上海机务段支配)
250 HXD3C 36 41 中国铁路武汉局集团有限公司 江岸机务段(武南机务段支配)
251 HXD3C 42 45 中国铁路武汉局集团有限公司 江岸机务段(襄阳机务段支配)
252 HXD3C 46 55 中国铁路南昌局集团有限公司 南昌机务段
253 HXD3C 56 60 中国铁路郑州局集团有限公司 郑州机务段
254 HXD3C 61 61 中国铁路沈阳局集团有限公司 沈阳机务段
255 HXD3C 62 62 中国铁路济南局集团有限公司 济南机务段
256 HXD3C 63 63 中国铁路沈阳局集团有限公司 沈阳机务段
257 HXD3C 64 70 中国铁路济南局集团有限公司 济南机务段
258 HXD3C 71 85 中国铁路武汉局集团有限公司 江岸机务段
259 HXD3C 86 95 中国铁路郑州局集团有限公司 郑州机务段
260 HXD3C 96 100 中国铁路济南局集团有限公司 济南机务段
261 HXD3C 101 110 中国铁路武汉局集团有限公司 江岸机务段
262 HXD3C 111 120 中国铁路沈阳局集团有限公司 沈阳机务段
263 HXD3C 121 125 中国铁路上海局集团有限公司 宁东机务段(上海机务段支配)
264 HXD3C 126 130 中国铁路南昌局集团有限公司 南昌机务段
265 HXD3C 131 135 中国铁路济南局集团有限公司 济南机务段
266 HXD3C 136 140 中国铁路郑州局集团有限公司 郑州机务段
267 HXD3C 141 165 中国铁路武汉局集团有限公司 江岸机务段
268 HXD3C 166 180 中国铁路成都局集团有限公司 重庆机务段
269 HXD3C 181 182 中国铁路济南局集团有限公司 济南机务段
270 HXD3C 183 190 中国铁路沈阳局集团有限公司 沈阳机务段
271 HXD3C 191 195 中国铁路济南局集团有限公司 济南机务段
272 HXD3C 198 200 中国铁路上海局集团有限公司 宁东机务段
273 HXD3C 201 220 中国铁路武汉局集团有限公司 江岸机务段
274 HXD3C 221 225 中国铁路上海局集团有限公司 宁东机务段
275 HXD3C 226 229 中国铁路沈阳局集团有限公司 沈阳机务段
276 HXD3C 238 238 中国铁路广州局集团有限公司 株洲机务段
277 HXD3C 271 300 中国铁路上海局集团有限公司 宁东机务段
278 HXD3C 446 446 中国铁路广州局集团有限公司 广州机务段
279 HXD3C 805 809 中国铁路广州局集团有限公司
280 HXD3C 810 819 中国铁路南宁局集团有限公司
281 HXD3C 820 829 中国铁路武汉局集团有限公司
282 HXD3C 896 925 中国铁路沈阳局集团有限公司
283 HXD3C 926 930 中国铁路南宁局集团有限公司
284 HXD3C 931 945 中国铁路北京局集团有限公司
285 HXD3C 946 955 中国铁路济南局集团有限公司
286 HXD3C 956 965 中国铁路郑州局集团有限公司
287 HXD3C 966 974 中国铁路济南局集团有限公司
288 HXD3D 1 10 中国铁路沈阳局集团有限公司 沈阳机务段
289 HXD3D 11 25 西安铁路局集团有限公司 西安机务段
290 HXD3D 26 34 中国铁路兰州局集团有限公司 兰州西机务段
291 HXD3D 35 35 中国铁路兰州局集团有限公司 迎水桥机务段 雷锋号
292 HXD3D 36 38 中国铁路兰州局集团有限公司 兰州西机务段
293 HXD3D 39 39 中国铁路济南局集团有限公司 济南机务段 共青团号
294 HXD3D 40 40 中国铁路兰州局集团有限公司 兰州西机务段
295 HXD3D 41 50 西安铁路局集团有限公司 西安机务段
296 HXD3D 51 70 中国铁路北京局集团有限公司 北京机务段
297 HXD3D 71 90 中国铁路南昌局集团有限公司 南昌机务段
298 HXD3D 91 115 中国铁路兰州局集团有限公司 兰州西机务段
299 HXD3D 116 135 中国铁路昆明局集团有限公司 昆明机务段
300 HXD3D 136 145 中国铁路北京局集团有限公司 北京机务段
301 HXD3D 146 150 呼和浩特铁路局集团有限公司 集宁机务段
302 HXD3D 151 155 中国铁路沈阳局集团有限公司 沈阳机务段
303 HXD3D 156 160 中国铁路南昌局集团有限公司 南昌机务段
304 HXD3D 161 165 中国铁路北京局集团有限公司 北京机务段
305 HXD3D 166 170 西安铁路局集团有限公司 西安机务段
306 HXD3D 171 180 中国铁路兰州局集团有限公司 兰州西机务段
307 HXD3D 181 190 中国铁路济南局集团有限公司 济南机务段
308 HXD3D 191 245 中国铁路沈阳局集团有限公司 沈阳机务段
309 HXD3D 246 255 中国铁路北京局集团有限公司 北京机务段
310 HXD3D 256 265 呼和浩特铁路局集团有限公司 集宁机务段
311 HXD3D 266 290 中国铁路兰州局集团有限公司 兰州西机务段
312 HXD3D 291 300 中国铁路沈阳局集团有限公司 沈阳机务段
313 HXD3D 301 310 中国铁路济南局集团有限公司 济南机务段
314 HXD3D 310 315 中国铁路昆明局集团有限公司 昆明机务段
315 HXD3D 316 320 中国铁路南昌局集团有限公司 南昌机务段
316 HXD3D 321 322 呼和浩特铁路局集团有限公司 集宁机务段
317 HXD3D 323 325 中国铁路北京局集团有限公司 北京机务段
318 HXD3D 326 333 西安铁路局集团有限公司 西安机务段
319 HXD3D 334 340 西安铁路局集团有限公司 安康机务段
320 HXD3D 341 345 中国铁路沈阳局集团有限公司 沈阳机务段
321 HXD3D 346 346 中国铁路成都局集团有限公司 重庆机务段
322 HXD3D 351 351 中国铁路成都局集团有限公司 重庆机务段
323 HXD3D 356 365 西安铁路局集团有限公司 安康机务段
324 HXD3D 366 369 中国铁路北京局集团有限公司 北京机务段
325 HXD3D 370 382 呼和浩特铁路局集团有限公司 集宁机务段
326 HXD3D 383 392 中国铁路昆明局集团有限公司 昆明机务段
327 HXD3D 393 397 中国铁路兰州局集团有限公司 兰州西机务段
328 HXD3D 398 402 西安铁路局集团有限公司 西安机务段
329 HXD3D 403 417 西安铁路局集团有限公司 西安机务段
330 HXD3D 418 419 中国铁路哈尔滨局集团有限公司 牡丹江机务段
331 HXD3D 420 424 中国铁路北京局集团有限公司 北京机务段
332 HXD3D 425 429 呼和浩特铁路局集团有限公司 集宁机务段
333 HXD3D 430 433 中国铁路哈尔滨局集团有限公司 牡丹江机务段
334 HXD3D 434 443 中国铁路南昌局集团有限公司 南昌机务段
335 HXD3D 444 449 中国铁路济南局集团有限公司 济南机务段
336 HXD3D 450 464 西安铁路局集团有限公司 西安机务段
337 HXD3D 465 468 中国铁路济南局集团有限公司 济南机务段
338 HXD3D 469 473 中国铁路济南局集团有限公司 济南机务段
339 HXD3D 474 479 中国铁路昆明局集团有限公司 昆明机务段
340 HXD3D 480 484 中国铁路南昌局集团有限公司 南昌机务段
341 HXD3D 485 489 西安铁路局集团有限公司 西安机务段
342 HXD3D 490 499 中国铁路北京局集团有限公司 北京机务段
343 HXD3D 500 503 中国铁路哈尔滨局集团有限公司 牡丹江机务段
344 HXD3D 504 514 中国铁路沈阳局集团有限公司 沈阳机务段
345 HXD3D 515 515 中国铁路成都局集团有限公司 重庆机务段
346 HXD3D 516 518 中国铁路沈阳局集团有限公司 沈阳机务段
347 HXD3D 519 528 西安铁路局集团有限公司 西安机务段
348 HXD3D 529 538 中国铁路北京局集团有限公司 北京机务段
349 HXD3D 539 541 呼和浩特铁路局集团有限公司 集宁机务段
350 HXD3D 542 553 西安铁路局集团有限公司 西安机务段
351 HXD3D 554 563 中国铁路济南局集团有限公司 济南机务段
352 HXD3D 564 568 中国铁路昆明局集团有限公司 昆明机务段
353 HXD3D 569 573 中国铁路成都局集团有限公司 重庆机务段
354 HXD3D 574 583 中国铁路济南局集团有限公司 济南机务段
355 HXD3D 584 584 中国铁路沈阳局集团有限公司 沈阳机务段
356 HXD3D 585 609 中国铁路哈尔滨局集团有限公司 三棵树机务段
357 HXD3D 610 611 中国铁路北京局集团有限公司 北京机务段
358 HXD3D 612 621 中国铁路南昌局集团有限公司 南昌机务段
359 HXD3D 622 626 中国铁路南昌局集团有限公司 南昌机务段
360 HXD3D 627 629 中国铁路哈尔滨局集团有限公司 三棵树机务段
361 HXD3D 630 630 中国铁路哈尔滨局集团有限公司 哈尔滨机务段
362 HXD3D 631 631 西安铁路局集团有限公司 西安机务段 第五代“钢人铁马号”
363 HXD3D 632 653 中国铁路沈阳局集团有限公司 沈阳机务段
364 HXD3D 654 673 中国铁路沈阳局集团有限公司 沈阳机务段
365 HXD3D 674 681 中国铁路沈阳局集团有限公司 沈阳机务段
366 HXD3D 682 688 中国铁路兰州局集团有限公司 兰州西机务段
367 HXD3D 1886 1886 中国铁路哈尔滨局集团有限公司 哈尔滨机务段 第五代“朱德号”
368 HXD3D 1893 1893 中国铁路北京局集团有限公司 丰台机务段 第六代“毛泽东号”
369 HXD3D 1921 1921 中国铁路沈阳局集团有限公司 沈阳机务段 共产党员号
370 HXD3D 7001 7002 广西沿海铁路股份有限公司 南宁南机务运用段
371 HXD3D 7003 7003 吉林铁道职业技术学院
372 HXD3D 8001 8025 中国铁路沈阳局集团有限公司 沈阳机务段 大同
373 HXD3D 8026 8028 中国铁路太原局集团有限公司 太原南机务段 大同
374 东方红2 1 50 资阳
375 东风 1201 1830 大连、成都
376 东风 2001 2094 戚墅堰
377 东风11 1 459 戚墅堰
378 东风12 8001 8001 吉林铁道职业技术学院
379 东风2 3201 3348 戚墅堰
380 东风21 1 5 中国铁路昆明局集团有限公司 昆明机务段
381 东风21 6 6 中国铁路昆明局集团有限公司 昆明机务段 状元号
382 东风21 7 7 中国铁路昆明局集团有限公司 昆明机务段 亲年号
383 东风21 8 8 中国铁路昆明局集团有限公司 昆明机务段 建水古城
384 东风21 9 100 中国铁路昆明局集团有限公司 昆明机务段
385 东风21 101 101 中国铁路昆明局集团有限公司 昆明机务段 异龙号
386 东风21 102 102 中国铁路昆明局集团有限公司 昆明机务段
387 东风21 1001 1002 云南钢铁厂
388 东风2Z 3251 3251 *齐齐哈尔铁路局 加格达奇机务段
389 东风3 3243 3243 中车共享城机车公园
390 东风4 3247 3247 中车成都轨道交通产业园
391 东风4B 1001 1999 大连
392 东风4B 1963 1963 *北京铁路局 丰台机务段
393 东风4B 2101 2685 大连
394 东风4B 2104 2104 *上海铁路局 蚌埠机务段
395 东风4B 2376 2376 *南昌铁路局 鹰潭机务段
396 东风4B 3101 3999 资阳
397 东风4B 3214 3214 *浙江金温铁道开发有限公司 温州机务段
398 东风4B 3249 3249 *西安铁路局 西安机务段
399 东风4B 3390 3390 *成都铁路局 重庆机务段
400 东风4B 3593 3593 *中国铁路广州局集团有限公司 株洲机务段
401 东风4B 6001 6587 大同
402 东风4B 6530 6530 *南宁铁路局 南宁机务段
403 东风4B 7001 7363 大连
404 东风4B 7364 7365 四方
405 东风4B 7366 7796 大连
406 东风4B 7701 7732 戚墅堰改
407 东风4B 9001 9702 资阳
408 东风4B 9167 9167 *南昌铁路局 向塘机务段
409 东风4B 9531 9531 *新长铁路公司
410 东风4C 1 10 大同
411 东风4C 11 11 中国铁路北京局集团有限公司 丰台段 青年文明号
412 东风4C 12 40 大同
413 东风4C 2001 2006 四方
414 东风4C 4001 4465 大连
415 东风4C 4466 4466 四方机车车辆厂 四方
416 东风4C 5001 5273 资阳
417 东风4C 5274 5275 三茂铁路公司 三水机务段 东风4CK
418 东风4C 5276 5335 资阳
419 东风4D 7001 7021 中国铁路南宁局集团有限公司
420 东风5 1 1 中国铁路北京局集团有限公司 北京车辆段
421 东风5 1974 1975 中国铁路兰州局集团有限公司 兰州西机务段 唐山
422 东风5 1976 2082 唐山
423 东风5 2083 2083 中国石油兰州石化公司 唐山
424 东风5 3279 3279 云南铁路博物馆
425 东风6 1 2 *沈阳铁路局 大连机务段
426 东风6 3 3 沈阳铁路陈列馆
427 东风6 4 4 *沈阳铁路局 大连机务段
428 东风7 174 174 中国铁路太原局集团有限公司 太原机务段北场
429 东风7B 3006 3006 中国铁道博物馆
430 东风7B 3015 3015 王坪村铁路公园
431 东风7B 6001 6072 *北京铁路局 邯郸机务段;郑州铁路局 新乡机务段 调车
432 东风7D 1 1 中国铁道博物馆
433 东风7D 3001 3001 中国铁道博物馆
434 东风7E 1 1 中国铁路郑州局集团有限公司 新乡机务段
435 东风7E 2 2 中国铁路郑州局集团有限公司 郑州机务段
436 东风7G 9001 9004 呼和浩特铁路局 集宁机务段 赛汗塔拉分段
437 东风8 1 1 中国铁道博物馆
438 东风9 1 2 中国铁路广州局集团有限公司广州机务段
439 韶山1 8 8 中国铁道博物馆
440 韶山1 156 156 郑州世纪欢乐园
441 韶山1 160 160 北京铁路电气化学校
442 韶山1 227 227 中国铁路兰州局集团有限公司 兰州西机务段
443 韶山1 254 254 中国铁路北京局集团有限公司 丰台机务段 储备厂
444 韶山1 307 307 中国铁路太原局集团有限公司 榆次机务折返段
445 韶山1 309 309 中国铁路太原局集团有限公司 太原机务段北场
446 韶山1 321 321 武汉铁路职业技术学院
447 韶山1 681 681 中国铁道博物馆
448 韶山1 695 695 沈阳铁路陈列馆
449 韶山1 762 762 中国铁路广州局集团有限公司 娄底运用车间储备厂
450 韶山1 818 818 西南交通大学 机车博物园
451 韶山1 821 821 韶关机务实训基地
452 韶山1 826 826 韶关机务实训基地
453 韶山3 454 454 中国铁路成都局集团有限公司 贵阳机务段 先锋号
454 韶山3 524 524 中国铁路武汉局集团有限公司 江岸机务段 青年号
455 韶山3 4160 4160 广西沿海铁路公司 南宁南机务运用段 共青团号
456 韶山3 4178 4178 广西沿海铁路公司 南宁南机务运用段 共青团号
457 韶山3 4235 4235 中国铁路成都局集团有限公司 重庆机务段 青年文明号
458 韶山3 4258 4258 中国铁路成都局集团有限公司 重庆机务段 党员先锋号
459 韶山3 5080 5080 广州铁路博物馆
460 韶山3 6005 6005 湖南交通工程学院
461 韶山3 8050 8050 武汉四美塘铁路遗址公园
462 韶山3B 16 16 西安铁路局 安康机务段 青年文明号
463 韶山3B 5001 5001 中国铁路成都局集团有限公司 贵阳机务段 *先锋力神
464 韶山3B 5035 5035 中国铁路兰州局集团有限公司 迎水桥机务段 雷锋号 (曾)
465 韶山3B 5038 5038 中国铁路兰州局集团有限公司 迎水桥机务段 青年文明号
466 韶山3B 5151 5151 中国铁路成都局集团有限公司 西昌机务段 扶贫先锋号
467 韶山3B 5162 5162 中国铁路昆明局集团有限公司 昆明机务段 五四青年号
468 韶山3B 5235 5235 中国铁路成都局集团有限公司 西昌机务段 *共青团号
469 韶山3C 1 1 中国铁路成都局集团有限公司 贵阳机务段
470 韶山4 6 6 中国铁道博物馆
471 韶山4 10 10 中国铁路成都局集团有限公司 西昌机务段
472 韶山4 50 50 中国铁路郑州局集团有限公司 新乡机务段 先锋号
473 韶山4 63 63 中国铁路太原局集团有限公司 太原机务段
474 韶山4 204 204 中国铁路郑州局集团有限公司 新乡机务段 先锋号
475 韶山4 448 448 中国铁路沈阳局集团有限公司 苏家屯机务段 先锋号
476 韶山4 574 574 中铁三局集团 先锋号
477 韶山4 743 743 中国铁路哈尔滨局集团有限公司 哈尔滨机务段 青年文明号
478 韶山4 855 855 西安铁路局 新丰镇机务段
479 韶山4 911 911 中铁三局集团 青年文明号
480 韶山4 2006 2006 吉林铁道职业技术学院
481 韶山4B 19 19 神朔铁路公司 神木北机务段 青年号
482 韶山4B 89 89 神朔铁路公司 神木北机务段 青年文明号
483 韶山4B 90 90 神朔铁路公司 神木北机务段 青年文明号
484 韶山4B 257 257 包神铁路公司 东胜机务段 党员先锋号
485 韶山4G 159 1177 株洲
486 韶山4G 168 168 中国铁道博物馆
487 韶山4G 171 171 中国铁路哈尔滨局集团有限公司 牡丹江机务段
488 韶山4G 179 179 中国铁路太原局集团有限公司 湖东机务段
489 韶山4G 466 466 石家庄铁道大学
490 韶山4G 1089 1089 *呼和浩特铁路局 包头西机务段
491 韶山4G 1886 1886 中国铁路哈尔滨局集团有限公司 哈尔滨机务段 *朱德号 株洲
492 韶山4G 3001 3002 资阳
493 韶山4G 6001 6001 中国铁道博物馆
494 韶山4G 6001 6001 中国铁道博物馆 大同
495 韶山4G 7001 7110 大连
496 韶山4G 7121 7243 大连
497 韶山5 1 1 中国铁道博物馆
498 韶山5 2 2 郑州世纪欢乐园
499 韶山6 1 1 郑州铁路司机学校
500 韶山6 2 2 中国铁道博物馆
501 韶山6B 1011 1011 西安铁路局 西安机务段 *青年文明号
502 韶山6B 1026 1026 韶关机务实训基地
503 韶山6B 1088 1088 中国铁路武汉局集团有限公司 襄阳机务段 *民兵号
504 韶山6B 1111 1111 中国铁路武汉局集团有限公司 襄阳机务段 *先锋号
505 韶山6B 6001 6001 韶关机务实训基地
506 韶山6B 6002 6002 广州铁路博物馆
507 韶山7 1 79 中国铁路南宁局集团有限公司 柳州机务段
508 韶山7 76 76 中国铁路南宁局集团有限公司 南宁机务段 *五四红旗号
509 韶山7 80 84 中国铁路南宁局集团有限公司 柳州机务段
510 韶山7 85 111 中国铁路南宁局集团有限公司 柳州机务段
511 韶山7 8112 8113 山西孝柳铁路有限责任公司
512 韶山7B 1 1 *南宁铁路局集团有限公司 南宁机务段
513 韶山7B 2 2 中国铁路南宁局集团有限公司 柳州机务段
514 韶山7D 1 58 西安铁路局集团有限公司 西安机务段
515 韶山7D 631 631 西安铁路局集团有限公司 西安机务段 *钢人铁马号
516 韶山7E 1 140 大同
517 韶山7E 6001 6002 中国铁路昆明局集团有限公司 大同
518 韶山7E 7001 7004 大连
519 韶山8 1 1 中国铁路广州局集团有限公司 广州机务段
520 韶山8 2 2 中国铁路广州局集团有限公司 广州机务段
521 韶山8 3 4 中国铁路上海局集团有限公司 上海机务段
522 韶山8 5 5 中国铁路郑州局集团有限公司 郑州机务段
523 韶山8 9 9 中国铁路郑州局集团有限公司 郑州机务段
524 韶山8 11 11 中国铁路郑州局集团有限公司 郑州机务段
525 韶山8 12 12 中国铁路郑州局集团有限公司 郑州机务段
526 韶山8 15 16 中国铁路郑州局集团有限公司 郑州机务段
527 韶山8 17 17 中国铁路上海局集团有限公司 上海机务段
528 韶山8 20 20 中国铁路郑州局集团有限公司 郑州机务段
529 韶山8 24 25 中国铁路郑州局集团有限公司 郑州机务段
530 韶山8 27 27 中国铁路郑州局集团有限公司 郑州机务段
531 韶山8 29 32 中国铁路郑州局集团有限公司 郑州机务段
532 韶山8 33 35 中国铁路上海局集团有限公司 上海机务段
533 韶山8 36 36 中国铁路郑州局集团有限公司 郑州机务段
534 韶山8 38 38 中国铁路上海局集团有限公司 上海机务段
535 韶山8 39 39 中国铁路上海局集团有限公司 上海机务段 国祥号
536 韶山8 40 40 中国铁路上海局集团有限公司 上海机务段
537 韶山8 41 41 中国铁路北京局集团有限公司 北京机务段
538 韶山8 43 43 中国铁路郑州局集团有限公司 郑州机务段
539 韶山8 44 44 中国铁路北京局集团有限公司 邯郸机务段
540 韶山8 45 45 中国铁路郑州局集团有限公司 郑州机务段
541 韶山8 48 48 中国铁路北京局集团有限公司 邯郸机务段
542 韶山8 49 49 中国铁路南昌局集团有限公司 南昌机务段
543 韶山8 50 50 中国铁路南昌局集团有限公司 南昌机务段
544 韶山8 51 51 中国铁路北京局集团有限公司 邯郸机务段
545 韶山8 52 52 中国铁路上海局集团有限公司 上海机务段
546 韶山8 55 55 中国铁路南昌局集团有限公司 南昌机务段
547 韶山8 56 57 中国铁路北京局集团有限公司 邯郸机务段
548 韶山8 64 64 中国铁路广州局集团有限公司 广州机务段
549 韶山8 72 72 中国铁路北京局集团有限公司 邯郸机务段
550 韶山8 73 73 中国铁路北京局集团有限公司 北京机务段
551 韶山8 74 74 中国铁路北京局集团有限公司 邯郸机务段
552 韶山8 81 81 中国铁路北京局集团有限公司 北京机务段
553 韶山8 83 84 中国铁路郑州局集团有限公司 郑州机务段
554 韶山8 85 85 中国铁路北京局集团有限公司 北京机务段
555 韶山8 88 103 中国铁路郑州局集团有限公司 郑州机务段
556 韶山8 104 104 中国铁路北京局集团有限公司 邯郸机务段
557 韶山8 109 111 中国铁路南昌局集团有限公司 南昌机务段
558 韶山8 114 116 中国铁路南昌局集团有限公司 南昌机务段
559 韶山8 118 119 中国铁路北京局集团有限公司 北京机务段
560 韶山8 121 126 中国铁路北京局集团有限公司 北京机务段
561 韶山8 127 128 中国铁路郑州局集团有限公司 郑州机务段
562 韶山8 130 130 中国铁路南昌局集团有限公司 南昌机务段
563 韶山8 131 131 中国铁路广州局集团有限公司 长沙机务段
564 韶山8 132 132 中国铁路广州局集团有限公司 长沙机务段
565 韶山8 133 133 中国铁路广州局集团有限公司 长沙机务段
566 韶山8 134 134 中国铁路广州局集团有限公司 长沙机务段
567 韶山8 136 136 中国铁路广州局集团有限公司 长沙机务段
568 韶山8 141 141 中国铁路广州局集团有限公司 广州机务段
569 韶山8 144 144 中国铁路广州局集团有限公司 长沙机务段
570 韶山8 148 148 中国铁路广州局集团有限公司 广州机务段
571 韶山8 156 156 中国铁路广州局集团有限公司 广州机务段
572 韶山8 163 163 中国铁路广州局集团有限公司 广州机务段
573 韶山8 166 166 中国铁路广州局集团有限公司 广州机务段 新世纪金龙号
574 韶山8 171 171 中国铁路上海局集团有限公司 上海机务段
575 韶山8 172 172 中国铁路郑州局集团有限公司 郑州机务段
576 韶山8 173 173 中国铁路广州局集团有限公司 广州机务段
577 韶山8 181 181 中国铁路广州局集团有限公司 广州机务段
578 韶山8 186 186 中国铁路广州局集团有限公司 广州机务段
579 韶山8 191 191 中国铁路广州局集团有限公司 广州机务段
580 韶山8 192 192 中国铁路广州局集团有限公司 广州机务段
581 韶山8 197 197 中国铁路郑州局集团有限公司 郑州机务段
582 韶山8 200 204 中国铁路上海局集团有限公司 上海机务段
583 韶山8 205 205 中国铁路广州局集团有限公司 长沙机务段
584 韶山8 214 214 中国铁路郑州局集团有限公司 郑州机务段
585 韶山9 1 3 中国铁路沈阳局集团有限公司 沈阳机务段;中国铁路上海局集团有限公司 上海机务段
586 韶山9 5 29 中国铁路沈阳局集团有限公司 沈阳机务段;中国铁路上海局集团有限公司 上海机务段
587 韶山9 30 30 中国铁路沈阳局集团有限公司 通辽机务段
588 韶山9 31 37 中国铁路沈阳局集团有限公司 沈阳机务段;中国铁路上海局集团有限公司 上海机务段
589 韶山9 38 38 中国铁路沈阳局集团有限公司 通辽机务段
590 韶山9 39 43 中国铁路沈阳局集团有限公司 沈阳机务段;中国铁路上海局集团有限公司 上海机务段

0
app/src/main/res/values/colors.xml Normal file → Executable file
View File

0
app/src/main/res/values/strings.xml Normal file → Executable file
View File

2
app/src/main/res/values/themes.xml Normal file → Executable file
View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.LBJReceiver" parent="android:Theme.Material.Light.NoActionBar" />
<style name="Theme.LBJConsole" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

0
app/src/main/res/xml/backup_rules.xml Normal file → Executable file
View File

0
app/src/main/res/xml/data_extraction_rules.xml Normal file → Executable file
View File

0
app/src/main/res/xml/file_paths.xml Normal file → Executable file
View File

View File

@@ -20,4 +20,4 @@ kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.nonTransitiveRClass=true

View File

@@ -8,6 +8,8 @@ espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.9.0"
activityCompose = "1.10.1"
composeBom = "2024.04.01"
room = "2.6.1"
startup = "1.1.1"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -24,9 +26,14 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
androidx-startup-runtime = { group = "androidx.startup", name = "startup-runtime", version.ref = "startup" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version = "2.0.0-1.0.21" }

0
gradlew vendored Normal file → Executable file
View File

View File

@@ -19,6 +19,6 @@ dependencyResolutionManagement {
}
}
rootProject.name = "LBJ Receiver"
rootProject.name = "LBJ_Console"
include(":app")