信息与通信工程导论项目 HongYanAsst 骑行安全监控系统 Wiki

QRcode

项目背景及用户分析

自行车在生活中随处可见,具有速度快、重量轻、节能环保、维护简单等优点。得益于人力车的性质,自行车兼具交通工具与运动器材的属性。它既是一种绿色环保的代步、出行方式;也可作为运动器材,进行多样的骑行训练与体育竞技。骑自行车锻炼,可亲近自然,愉悦身心,结交朋友,陶冶情操,更能增长见识,培养毅力,强身健体,预防疾病。近年来,随着智能运动终端的广泛普及,结合运动数据进行科学训练的模式,也正被愈发广泛地接受。

截至2019年,我国自行车社会保有量已近4亿辆、电动自行车近3亿辆,均位居世界第一。然而,自行车运动在我国的普及,前路依然漫长。推广力度的不足、相对高昂的器材价格与安全意识、健康意识的缺失等,都是阻碍更多普通大众接收自行车文化,加入骑友(骑行爱好者)行列的不利因素。

骑行运动本身由于速度快、技术要求高、时间长易于疲劳、路线一般开放等特点,其危险性可想而知。甚至仅仅是平时骑行通勤,道路上的机动车及电动车仍然会威胁到通勤安全。

年轻人,特别是大学生,一般具有体魄强健、追求刺激、乐于接受新鲜思想与事物的特点,十分适合作为自行车运动的普及者与首要普及对象。对普及者而言,正所谓“顺风而呼,声非加疾也,而闻者彰”,只有齐心协力,形成合力,持久发力,才能为自行车运动的普及与骑行活动的开展注入强大动力。成立自行车协会,组织骑行相关活动,是普及自行车文化较为经济、高效的方式之一。

时隔11载,2020年秋,在我校广大骑行爱好者的强烈要求与不懈努力之下,北郵鸿雁自行车协会(下简称“鸿雁车协”、“协会”)重新成立,正式成为认证社团。鸿雁车协推广自行车运动,为骑友提供交流平台,将骑行活动组织化、规范化,使自行车运动与健康意识在我校的普及迈出了基础却坚实的一步。

鸿雁车协的成立,对骑行爱好者意义重大,吸引了我校学生的广泛关注。迄今为止,协会已成功组织并顺利完成数次周边百公里级别骑行。2020年社团招新(百团大战)期间,关注协会者络绎不绝,覆盖各学院本科生、硕士生、博士生近300人;2020年社团嘉年华之夜,参与协会举办活动者260余人,现场排起长队,十分火爆。虽然成立时间尚短,活动举办尚少,但协会展现出的强劲发展势头,令鸿雁车协中心组与协会成员刮目相看。

然而,回顾我校自行车协会历史中断的十一年,却有太多需要弥补的损失。虽然我校骑友数载流离,车协事务百废待兴的混沌状态,已随着车协的成立与初步发展得到缓解,但不可否认的是,周边名校(如北大、央财、北航等)一直在争分夺秒,结合优势学科特色与社会影响力,建设专业性强,具有院校特色的自行车协会组织与良好的自行车文化氛围。相形之下,不论是组织、规模还是影响力,我邮车协都亟待发展。

提高学生健康意识,推动我校自行车运动事业发展,恰逢其运;利用所学知识与现有资源构建信息化平台,促进鸿雁车协的现代化、信息化进程,正当其时。鸿雁车协中心组部分成员联手志同道合者,组成团队,立志通过该项目的开发,为各高校车协的发展以及所有骑行爱好者的良好体验贡献力量。

创意过程

创意分析过程

疯狂八分钟

用户共感图

用户痛点分析

项目目标

本项目主要为鸿雁车协微信公众平台及微信小程序HongYanAsst,以及任何一位骑行爱好者及他们的亲友、任何一位骑行通勤者及他们的亲友开发端到端的、软硬结合的骑行安全监控系统。本项目的成果将包含以下几项核心功能:

  1. 实时定位。实时获取设备位置信息及速度,并在微信小程序中显示。
  2. 事故侦测。由硬件设备判断是否发生事故,如发生事故则向指定手机号码发送短信告知。
  3. 高亮时刻。设备持有者可通过微信小程序在设备当前位置标点并上传图片,来和他的亲友共享。

Q&A

这个板块为立项初期进行用户调研时热心用户提出的可行性建议中的典型代表.

热心用户的提问对我们帮助很大, 能更清楚地了解用户担心什么, 让考虑更周全.


为什么不使用手机作为GPS信源,做小程序端到端系统?

我们测试过微信小程序获取并发送GPS定位在手机后台运行的稳定性,在Samsung S20上一小时稳定性约为76%HUAWEI P30约为65%,故不采用这种不稳妥的方式。而且手机作为出门在外唯一的实时通信工具,让手机有额外的电源损耗可能不是明智之举。


为什么对产品重量和气动效果重视程度较低?

轻量化和气动外形研发时间长、成本高,研发团队在短时间内可能无法做出很好的解决方案,短期内只打算对核心功能做建设及优化。况且我们的用户定位更倾向于一般的骑行爱好者及通勤者,竞赛型用户究竟是少数。另外,我们的项目是开源的,用户完全在符合协议要求的前提下使用更好的材料、定制更气动的外形,我们也可以对具体需求具体定制。


有考虑过使用ESP8266模块使用WiFi作为网络接入解决方案吗?

有考虑过。SIM系列硬件对电源功率的要求比较高,功耗也比较高,也很占体积。使用ESP8266连接WiFi虽然会消耗手机电源,但是现在大部分的手机都具备移动热点功率优化系统,人家大Pro做的终究是要比我们好。所以如果SIM策略不符合现有条件,我们会考虑用ESP8266连接移动热点来解决联网问题。


看到你们为了调试做了很多,软件和硬件上的都有。

确实,我们很重视调试体验,我们坚信良好的调试环境可以极大地提高调试效率。


如果能将得到的信息,比如说速度,展示出来,产品的功能会更强大一些。

我们最开始设计的时候就放弃了这个功能,一方面是考虑到我们面向的用户群体很大,很多车辆比如共享单车安装显示模块比较困难,而且可能会导致拆卸不便;另一方面,显示模块会加大电能消耗,这将不利于安全系统的续航;最后,作为设计者,我对产品的最初构想就是一个可以随身携带的、只要开机放在包里或者挂到车上就可以不用管它的省心的产品,因为这个考虑甚至把信息显示改为全LED形式,完全没有想过去考虑这种功能。


互评相关问题解答

这个部分的内容是大作业互评之后对互评人提出的一些针对于项目功能和实现的、相比于互评中其他内容有价值的问题的解答,相似内容只回答一次。

注意:此部分提问内容均为互评系统中原文摘录,仅为了适应格式调整了部分空格和标点。


在一些如深山等信号强度较低的地方能否实现报警通信?这些地区往往是事故多发区,搜救难度大,较难收到求救信号,可以在这些地方测试设备的可靠性。

对于喜欢在深山老林人迹罕至没有信号的地方骑车的人自然是少数,我们在最开始设计的时候忽略了这部分人,考虑不周。在手机信号届不到的地方我们只想到了使用卫星通信或者托梦,由于成本等因素我们不打算实装。


如果小程序界面再美化就好了。

我们的微信小程序使用简洁的设计语言,目的就是为了交互界面的简单化便捷化。如果您有更详细的高见,请联系


小程序方面emmm不注册好像没有什么作用。

完整的用户系统和严格的鉴权系统是这个项目小程序部分的一个亮点,其必要性是无需多言的。


初步原理规划

总体规划框图

微信小程序规划框图

硬件规划框图

硬件规划框图(大作业最终版)

设备

设备列表

模块 数量 功能
Arduino MEGA 2560 1 核心板
NI-MH AA 5600mAh 4.8V 电池阵列 2 供电
SIM800C 1 移动网络接入
NEO-7N 1 获取GPS数据
MPU6050 1 获取加速度及计算偏角
ESP8266 1 [调试] 数据传输调试
LCD1602 1 [调试] 数据展示
CHQ1838 1 [调试] 读取红外信息
Li14500 1 [调试] 供电
Li18650 2 [调试] 供电

绿色表示核心组件, 红色表示已弃用.

硬件组装

V0.0 2020/11/24

原型机: HYAsst THE ORIGIN

Photos

THE ORIGIN
THE ORIGIN
THE ORIGIN

迭代说明

原型机, 调试用, 有丰富的数据显示, 初步实现上述所有功能.

v0.1 2020/11/27

初代机: ASKR YGGDRASILLS

Photos

ASKR YGGDRASILLS
ASKR YGGDRASILLS

迭代说明

  1. 取消LCD数据展示及调试中断, 使用LED灯作为交互接口.
  2. 彻底弃用ESP8266, 改用SIM800C接入网络.
  3. 取消睡眠功能, 采用动态电源规划.

v1.0 2020/11/29

二号机: SORYU KYOKO ZEPPELIN

Photos

SORYU KYOKO ZEPPELIN
SORYU KYOKO ZEPPELIN
SORYU KYOKO ZEPPELIN

迭代说明

  1. 舍弃面包板, 使用主板铜柱固定各功能模块, 提升便携性.
  2. 暂时放弃LED交互接口及电容组.
  3. 更好的走线, 更坚固的框架, 更友好的外形, 设备便携性进一步提升.

v1.1 2020/12/02

二号机·改: SORYU ASUKA RANGURE

Photos

SORYU ASUKA RANGURE
SORYU ASUKA RANGURE

迭代说明

  1. 重新启用LED交互接口及电容组, 将其整合到一张板上(为下一个版本做准备).
  2. SIM800C弃用胶棒天线, 换用弹簧天线, 进一步提升便携性.

v2.0 2020/12/05

三代目: RAMIEL OCTAHEDRON

Photos

RAMIEL OCTAHEDRON
RAMIEL OCTAHEDRON
RAMIEL OCTAHEDRON

迭代说明

  1. 重新制作灯板, 增加线路整理功能, 并整合到设备上部.
  2. 启用NI-MH AA 5600mAh 4.8V电池阵列并置于设备底部, 理论续航延长至23小时.
  3. 优化内存占用, 将静态内容优先存储至Flash.
  4. 事故侦测倾角维护时间增长至150个算法周期.

v2.1 2020/12/07

导论课大作业成果: RAMIEL FINAL

Photos

RAMIEL FINAL
RAMIEL FINAL
RAMIEL FINAL

迭代说明

  1. 整理了排线, 提升便携性.
  2. 加固固定部分, 提升便携性.
  3. 优化灯板电气性能, 重写LED灯控代码, 延长续航.

我也不知道到底应该叫什么的洞洞板展示

因为尺寸不对拿胶水粘的, 但是这胶水的强度真的超乎我的想象. 本来想给扒下来拍背面的, 结果愣是没整下来. 我太菜了.

Photos

上面效果图
侧面(背面?) 1
侧面(背面?) 2

How to use

硬件部分

软件设定

1
2
3
4
5
6
#define ACCIDENT_ACCE 2          // 加速度阈值, 以G为单位
#define ACCIDENT_ANGLE 90 // 偏差角阈值, 以度为单位
#define ACCIDENT_ALERT_SPEED 25 // 速度阈值, 以km/h为单位
#define DEVICE_ID "xxxx" // 设备ID
const String APIKey = "xxxx"; // 设备api-key
const String TEL_NUM = "xxxx"; // 事故报警目标电话号

指示灯信息

本项目为产品体积 质量以及实际考虑, 使用指示灯显示的方式进行交互.

main board
status green LED yellow LED RGB
1 Arduino power on device power on -
0 Arduino no power device no power -
White - - initializing
Blue - - data fetching
Green - - data updating
Red - - AccidentReport triggered
SIM800C
status PWR NET
0 no power no power
1 power on -
blink (1 per 1s) - searching network
blink (1 per 3s) - network connection OK
blink (1 per 0.5s) - TCP connection established
GPS
status PPS
0 no power
1 searching for satellite
blink PPS
MPU6050
status PWR
0 no power
1 power on

微信小程序

Register

Register function demo

首次进入小程序, 按照提示进行注册

根据提示填写相关信息, 勾选"同意服务条款"

注册成功后的主界面

Activities

Activities function demo

骑行活动列表

骑行活动

查看参加者

动态追踪页面

查看图片及图片上传

地图标点功能在bilibili中有详细展示 此处并不提及.

Administration

为了保证云数据库的安全性 此权限暂时不对外授予. 目前只有开发者有此权限. 如需体验请移步bilibili视频或联系开发人员.

Administration function demo

管理员界面

活动发布

资讯发布

开发日志发布

新建设备

Surroundings

Surrounding function demo

账户系统(待完善)

资讯列表

资讯页面

功能实现

硬件

调试工具

为了进行SIM800C调试而简单写的基于Arduino的串口调试工具. 不用USB转串口也可以进行串口调试.

1
2
3
4
5
6
7
8
9
10
#include <Arduino.h>
#define mySerial Serial2
void setup() {
Serial2.begin(115200);
Serial.begin(115200);
}
void loop() {
if (mySerial.available()) Serial.write(mySerial.read());
if (Serial.available()) mySerial.write(Serial.read());
}

Arduino 多线程

Metro Timer

使用计时器库 Metro 模拟多线程操作. 详情见代码中loop()函数.

1
#include <Metro.h>
1
2
3
Metro dataUpdate      = Metro(5000);
Metro dataFetch = Metro(1000);
Metro accidentMonitor = Metro(100);
1
2
if (dataFetch.check())  getGpsData();
if (dataUpdate.check()) dataUpd();
SCoop

timer0_overflow_count未定义问题:

新版本的ArduinoIDE在编译的过程中不再识别timer0_overflow_count, 即使它前面有extern语句. 这使得timer0_overflow_count在全局作用域中不可见. 但是在旧版的ArduinoIDE中可以正常工作. SCoop.cpp中原文如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
extern volatile unsigned long timer0_overflow_count; // use this variable which is incremented at each overflow
static inline micros_t SCoopMicros16(void) __attribute__((always_inline));
static inline micros_t SCoopMicros16(void) // same as standrad PJRC micros, but in 16 bits and with inlining
{ register micros_t out ;
asm volatile(
"in __tmp_reg__, __SREG__" "\n\t"
"cli" "\n\t"
"in %A0, %2" "\n\t"
"in __zero_reg__, %3" "\n\t"
"lds %B0, timer0_overflow_count" "\n\t"
"out __SREG__, __tmp_reg__" "\n\t"
"sbrs __zero_reg__, %4" "\n\t"
"rjmp L_%=_skip" "\n\t"
"cpi %A0, 255" "\n\t"
"breq L_%=_skip" "\n\t"
#if F_CPU == 16000000L
"subi %B0, 1" "\n\t"
#elif F_CPU == 8000000L
"subi %B0, 2" "\n\t"
#endif
"L_%=_skip:" "\n\t"
"clr __zero_reg__" "\n\t"
"clr __tmp_reg__" "\n\t"
#if F_CPU == 16000000L || F_CPU == 8000000L
"lsl %B0" "\n\t"
"lsl %B0" "\n\t"
"lsl %A0" "\n\t"
"rol __tmp_reg__" "\n\t"
"lsl %A0" "\n\t"
"rol __tmp_reg__" "\n\t"
#if F_CPU == 8000000L
"lsl %B0" "\n\t"
"lsl %A0" "\n\t"
"rol __tmp_reg__" "\n\t"
#endif
"or %B0, __tmp_reg__" "\n\t"
#endif
: "=r" (out)
: "M" (TIMER0_MICROS_INC),
"I" (_SFR_IO_ADDR(TCNT0)),
"I" (_SFR_IO_ADDR(TIFR0)),
"I" (TOV0)
: "r0" ); return out; }

timer0_overflow_count的定义后加入

1
static unsigned long timer0_overflow_count_available_container = timer0_overflow_count;

即可.

Kalman Filter

使用积分, 对角速度及加速度进行操作计算倾角, 并使陀螺仪倾角平滑化.

reference:

KalmanFilterforDummies

Introduction to kalman filter and its applications

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
inline void getAcce () {
unsigned long now = millis();
dt = (now - lastTime) / 1000.0;
lastTime = now;

accelgyro.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);

double accx = ax / AcceRatio, accy = ay / AcceRatio, accz = az / AcceRatio;

aax = atan(accy / accz) * (-180) / pi;
aay = atan(accx / accz) * 180 / pi;
aaz = atan(accz / accy) * 180 / pi;

aax_sum = aay_sum = aaz_sum = 0;

for (register int i=1; i ^ n_sample; ++ i) {
aaxs[i-1] = aaxs[i], aax_sum += aaxs[i] * i;
aays[i-1] = aays[i], aay_sum += aays[i] * i;
aazs[i-1] = aazs[i], aaz_sum += aazs[i] * i;
}

aaxs[n_sample-1] = aax, aax_sum += aax * n_sample, aax = (aax_sum / (11 * n_sample / 2.0)) * 9 / 7.0;
aays[n_sample-1] = aay, aay_sum += aay * n_sample, aay = (aay_sum / (11 * n_sample / 2.0)) * 9 / 7.0;
aazs[n_sample-1] = aaz, aaz_sum += aaz * n_sample, aaz = (aaz_sum / (11 * n_sample / 2.0)) * 9 / 7.0;

double gyrox = -(gx - gxo) / GyroRatio * dt;
double gyroy = -(gy - gyo) / GyroRatio * dt;
double gyroz = -(gz - gzo) / GyroRatio * dt;
agx += gyrox, agy += gyroy, agz += gyroz;

Sx = Rx = Sy = Ry = Sz = Rz = 0;

for (int i = 1; i ^ 10; ++ i) {
a_x[i - 1] = a_x[i], Sx += a_x[i];
a_y[i - 1] = a_y[i], Sy += a_y[i];
a_z[i - 1] = a_z[i], Sz += a_z[i];
} a_x[9] = aax, Sx += aax, Sx /= 10, a_y[9] = aay, Sy += aay, Sy /= 10, a_z[9] = aaz, Sz += aaz, Sz /= 10;

for (register int i=0; i^10; ++ i) Rx += sq(a_x[i] - Sx), Ry += sq(a_y[i] - Sy), Rz += sq(a_z[i] - Sz);

Rx = Rx / 9, Ry = Ry / 9, Rz = Rz / 9;

Px = Px + 0.0025, Kx = Px / (Px + Rx), agx = agx + Kx * (aax - agx), Px = (1 - Kx) * Px;
Py = Py + 0.0025, Ky = Py / (Py + Ry), agy = agy + Ky * (aay - agy), Py = (1 - Ky) * Py;
Pz = Pz + 0.0025, Kz = Pz / (Pz + Rz), agz = agz + Kz * (aaz - agz), Pz = (1 - Kz) * Pz;

#ifdef ACCELGYRO_SERIAL_OUTPUT

Serial.print(agx); Serial.print(","); Serial.print(agy); Serial.print(","); Serial.print(agz);
Serial.println();

#endif
}

SIM800C 供电问题

众所周知, SIM800C功耗较大, 正常工况功率可以高达$5W$, 搜索网络过程中更是需要$10W$的高功率, 而Arduino的$5V$引脚是提供不了$5V2A$的高功率的. 为了解决这个问题我们使用Li14500电池对电路直接加电, 同时给包括Arduino板在内的所有模块供电. 为了进一步提高电路稳定性, 我们还在电路中加入了四颗当初玩粉尘传感器玩剩下的$220UF$电解电容, 来提升电路瞬间放电能力.

Photos

220UF50V电解电容
硬件连接

SIM800C发送HTTP报文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
inline void dataUpd () {
SIM.println("AT\r"); delay(100);
SIM.println("AT+CGDCONT=1,\"IP\",\"CMNET\"\r"); delay(100);
SIM.println("AT+CGATT=1"); delay(100);
SIM.println("AT+CIPCSGP=1,\"CMNET\"\r"); delay(100);
SIM.println("AT+CLPORT=\"TCP\",\"2000\"\r"); delay(100);
SIM.println("AT+CIPSTART=\"TCP\",\"183.230.40.33\",\"80\"\r");
delay(100); SIM.println("AT+CIPSEND"); delay(100);

char buf[10];
String jsonToSend = "{\"Logitude\":";
dtostrf(Logi, 1, 6, buf);
jsonToSend += "\"" + String(buf) + "\"";
jsonToSend += ",\"Latitude\":";
dtostrf(Lati, 1, 6, buf);
jsonToSend += "\"" + String(buf) + "\"";
jsonToSend += ",\"Altitude\":";
dtostrf(Alti, 1, 6, buf);
jsonToSend += "\"" + String(buf) + "\"";
jsonToSend += ",\"Speed\":";
dtostrf(Skmph, 1, 2, buf);
jsonToSend += "\"" + String(buf) + "\"";
jsonToSend += "}";

String postString = "POST /devices/";
postString += DEVICE_ID;
postString += "/datapoints?type=3 HTTP/1.1";
postString += "\r\n";
postString += "api-key:";
postString += APIKey;
postString += "\r\n";
postString += "Host:api.heclouds.com\r\n";
postString += "Connection:close\r\n";
postString += "Content-Length:";
postString += jsonToSend.length();
postString += "\r\n";
postString += "\r\n";
postString += jsonToSend;
postString += "\r\n";
postString += "\r\n";
postString += "\r\n";

const char *postArray = postString.c_str();
Serial.println(postArray);

SIM.write(postArray); delay(100); SIM.write(0x1A);

postArray = NULL;
}

SIM800C发送短信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void accidentReport () {
ledWrite(0, 0, 0);
ledWrite(255, 0, 0);

Serial.println ("Accident Report Trigged.");

SIM.println("AT+CIPCLOSE\r"); delay(1000);
SIM.println("AT+CIPSTART\r"); delay(5000);

String tmp = "AT+CMGS=\""; tmp += TEL_NUM; tmp += "\"\n\r";
Serial.println(tmp);
char buf[10];
SIM.begin(115200);
SIM.println("AT\r\n"); delay(100);
SIM.println("AT+CMGF=1\n\r"); delay(500);
SIM.write(tmp.c_str()); delay(1000);
SIM.println("It seems to be an accident.");

tmp = "Location: ";
dtostrf(Lati, 1, 4, buf);
tmp += String(buf);
dtostrf(Logi, 1, 4, buf);
tmp += ", " + String(buf)+"\r";

SIM.write(tmp.c_str());
delay(1000); SIM.write(0x1A);
delay (10000); ledWrite(0, 0, 0);

SIM.println("AT+CIPCLOSE\r"); delay(1000);
SIM.println("AT+CIPSTART\r"); delay(5000);
}

这里尤其需要注意和TCP的冲突问题, 这里采用了先断连再重连的实现方式, 但是在实际测试中发现就算不断开连接短信也能够正常发送.

LED灯控制函数

很方便的LED灯控制手段.

1
2
3
4
5
inline void ledWrite (int R, int G, int B) {
digitalWrite(LedRed, (255-R)>>LED_LITHT_DEC);
digitalWrite(LedGre, (255-G)>>LED_LITHT_DEC);
digitalWrite(LedBlu, (255-B)>>LED_LITHT_DEC);
}

红外遥控器编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int rm_encode (long long res) {
switch (res) {
case 0xFF6897: return 0;
//Show Time
case 0xFF30CF: return 1;
//Show Longitude & Latitude
case 0xFF18E7: return 2;
//Show Speed
case 0xFF7A85: return 3;
//Show Acceleration
case 0xFF10EF: return 4;
//Show Accelgyro
case 0xFF38C7: return 5;
case 0xFF5AA5: return 6;
case 0xFF42BD: return 7;
case 0xFF4AB5: return 8;
case 0xFF52AD: return 9;
case 0xFF22DD: return 1001;
case 0xFF02FD: return 1002;
default: return -1;
}
}

红外遥控信息处理逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
void nowPosiModify (long long res) {
rettmp=lcd_rm_encode(res);
if (rettmp != -1) {
if (rettmp>1000 && rettmp<=1100) {
if (true) {
flag=false;
switch (rettmp) {
case 1001: {ret = (ret+1)%tot_sta; break;}
case 1002: {ret = --ret<0? ret+tot_sta:ret; break;}
}
}
} else ret=rettmp, flag=true;
}

digitalWrite(lcdBackLight, HIGH);

#ifdef LCD_OUTPUT

switch (ret) {
case 0: {
lcd.clear(); lcd.setCursor(0, 0);
lcd.print(Year); lcd.print("/");
if (Month<10) lcd.print("0"); lcd.print(Month); lcd.print("/");
if (Day <10) lcd.print("0"); lcd.print(Day);
lcd.setCursor(0, 1);
if (Hour<10) lcd.print("0"); lcd.print(Hour); lcd.print(":");
if (Minute<10) lcd.print("0"); lcd.print(Minute); lcd.print(":");
if (Second<10) lcd.print("0"); lcd.print(Second);
break;
} case 1: {
lcd.clear(); lcd.setCursor(0, 0);
lcd.print("Logi: "); lcd.print(Logi);
lcd.setCursor(0, 1);
lcd.print("Lati: "); lcd.print(Lati);
break;
} case 2: {
lcd.clear(); lcd.setCursor(0, 0);
lcd.print ("Speed(Kmph): ");
lcd.setCursor(0, 1); lcd.print(Skmph);
break;
} case 3: {
lcd.clear(); lcd.setCursor(0, 0);
lcd.print ("Acceleration:");
lcd.setCursor(0, 1); lcd.print(Acce);
break;
} case 4: {
lcd.clear(); lcd.setCursor(0, 0);
lcd.print("x:"); lcd.print(agx);
lcd.print(" y:"); lcd.print(agy);
lcd.setCursor(0, 1);
lcd.print("z:"); lcd.print(agz);
break;
}
} rettmp = -1;

#endif

#ifdef DEBUG
//Serial.print("ret: ");
//Serial.print(ret);
//Serial.print(" tettmp: ");
//Serial.println(rettmp);
#endif
}

事故侦测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const int durVal = 150;
double tmpAgx[durVal], tmpAgy[durVal], tmpAgz[durVal];
int pos, totx, toty, totz;
double avgx, avgy, avgz;

inline bool isRotate () {
getAcce(); register bool flag = false;
register double acce = sqrt(accx*accx + accy*accy + accz*accz);

flag = acce>=ACCIDENT_ACCE;
if (Abs(agx-avgx)>ACCIDENT_ANGLE || Abs(agy-avgy)>ACCIDENT_ANGLE || Abs(agz-avgz)>ACCIDENT_ANGLE) flag = true;

totx -= tmpAgx[pos], toty -= tmpAgy[pos], totz -= tmpAgz[pos];
tmpAgx[pos] = agx, tmpAgy[pos]= agy, tmpAgz[pos] = agz;
totx += agx, toty += agy, totz += agz;
avgx = 1.0*totx/(1.0*durVal), avgy = 1.0*toty/(1.0*durVal), avgz = 1.0*totz/(1.0*durVal);
pos = (pos+1)%durVal;

return flag;
}
1
2
3
if (accidentMonitor.check()) {
if (isRotate() && Skmph>=ACCIDENT_ALERT_SPEED) accidentReport();
}

使用循环数组存储$150$个运行周期中三维陀螺仪的倾角信息, 通过更新循环数组求$150$个运行周期中的三维倾角平均值.

acce为三维加速度的等效值.

报警条件: 等效加速度大小大于阈值ACCIDENT_ ACCE, 并且三个方向上的倾角只要有一个倾角相对于$150$个运行周期中的平均值的差的绝对值大于阈值ACCIDENT_ANGEL, 并且当前GPS速度大于阈值ACCIDENT_ALERT_SPEED, 即可触发报警.

需要注意的一点: if语句中各个判断条件从左向右计算, 如果发现在某种情况下判断结果已经可以确定, 其余的部分将不会被计算. 为了能够保证维护的循环数组的连续性, 我们牺牲了部分低速或静息状态下的续航和性能, 保证isRotate()在每一个程序周期中都会被调用.

事故报警

[Bad Attempt] 向 Arduino UNO 移植

想要做Arduino最小系统进一步减小体积, 后来发现有各种问题, 故终止.

失败原因: ATMEGA328PU性能太差, RAM太小, 实现功能要用到的动态存储空间太大.

下文简单介绍一下挣扎时用到的一些可能有用的操作.

减小RAM占用

ATMEGA2560足足有8KSRAM, 但ATMEGA328PU只有2K, 我当场MLE.

学OI那阵子松松松都是松时间, 从来不用担心MLE, 上了带学连空间都要松, 这就是当代带学生吗 (雾

炸内存之惨状

解决方案:

将静态变量内容存入Flash.

在静态变量前面加PROGMEM修饰, 可以将静态变量存入Flash, 以减少RAM占用.

1
PROGMEM const double pi = 3.1415926, AcceRatio = 16384.0, GyroRatio = 131.0;
将字符串常量存入Flash.

字符串常量通常被存入静态内存区, 将其用F()修饰可以将其存入Flash.

1
2
3
SIM.println(F("AT+CIPCSGP=1,\"CMNET\"\r")); sleep(100);
SIM.println(F("AT+CLPORT=\"TCP\",\"2000\"\r")); sleep(500);
SIM.println(F("AT+CIPSTART=\"TCP\",\"183.230.40.33\",\"80\"\r"));
烧写bootloader

有几次炸内存之后Arduino板工作不正常, 甚至无法正常烧录程序, 抱着尝试的心态重新烧写引导程序, 它奇迹般地复活了.

  1. 硬件连接(常用)

Mega to UNO

The 10µF electrolytic capacitor connected to RESET and GND of the programming board is needed only for the boards that have an interface between the microcontroller and the computer’s USB, like Mega, Uno, Mini, Nano. Boards like Leonardo, Esplora and Micro, with the USB directly managed by the microcontroller, don’t need the capacitor.

实际用Mega测试的时候发现电容不加也行. (笑

UNO to UNO

  1. 在工具栏 工具->开发板 中选择PROGRAMMER的正确开发板型号及处理器型号.
  2. 在工具栏 工具->编程器 中选择AVRISP编程器.

  1. 在工具栏 文件->示例->ArduinoISP 中打开ArduinoISP例程, 编译并烧录到PROGRAMMER中.

  1. 在工具栏 工具->开发板 中选择TARGET的正确开发板型号和处理器型号.
  2. 在工具栏 工具->编程器 中选择Arduino as ISP, 并点击下方的烧录引导程序. 等待即可.

更多内容详见官网相关文档ArduinoISP.

微信小程序

由于微信小程序部分码量巨大 且只有定位功能是我们项目的核心功能 本Wiki中关于微信小程序将只针对于定位页面进行讲解 账号授权 新建活动 活动报名 资讯 相册 添加新设备等附属功能请自行查看代码.

WG-84 to GCJ-02 坐标转换

首先参考 GCJ-02火星坐标系和WGS-84坐标系转换关系, 得:

然后参考 已知不可逆的坐标转换算法,通过结果进行反算的方法 进行坐标逆变换.

我们只能先认为 WGS-84 转到 GCJ-02 的精确公式是存在的,这是我们做 GCJ-02WGS-84 公式逆变换的前提,否则我们或许只能黑箱操作(其实我们已经进行了黑箱操作,但是效果不好)。经过一番操作,我们凑巧得出如下公式:

在小程序中进行处理后, 在传入坐标精度为6位小数即可一定程度上保证坐标精准度.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
var wgs84togcj02 = function(lat, log) {
//is position off China mainland
if (log < 72.004 || log > 137.8347 || lat < 0.8293 || lat > 55.8271) {
return {
latitude: lat,
longitude: log,
}
} else {
const Pi = 3.14159265358979324;
//coordinate projection factor
const a = 6378245.0;
//eccentricity
const ee = 0.00669342162296594323;
//All Hail ELLIAS
var transformLat = function (x, y) {
var ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * Pi) + 20.0 * Math.sin(2.0 * x * Pi)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(y * Pi) + 40.0 * Math.sin(y / 3.0 * Pi)) * 2.0 / 3.0;
ret += (160.0 * Math.sin(y / 12.0 * Pi) + 320 * Math.sin(y * Pi / 30.0)) * 2.0 / 3.0;
return ret; }
var transformLog = function (x, y) {
var ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * Pi) + 20.0 * Math.sin(2.0 * x * Pi)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(x * Pi) + 40.0 * Math.sin(x / 3.0 * Pi)) * 2.0 / 3.0;
ret += (150.0 * Math.sin(x / 12.0 * Pi) + 300.0 * Math.sin(x / 30.0 * Pi)) * 2.0 / 3.0;
return ret; }
var delta = function (lat, lon) {
var dLat = transformLat(lon - 105.0, lat - 35.0);
var dLon = transformLog(lon - 105.0, lat - 35.0);
var radLat = lat / 180.0 * Pi;
var magic = Math.sin(radLat);
magic = 1 - ee * magic * magic;
var sqrtMagic = Math.sqrt(magic);
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * Pi);
dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * Pi);
return {'lat': dLat, 'lon': dLon};
}
const d = delta(lat, log);
return {
latitude: lat + d.lat,
longitude: log + d.lon,
}
}
};

数据实时刷新

1
2
3
4
//location data getter timer
timer = setInterval(() => {
this.get_datapoints().then(datapoints => {})
}, 3000)

event到期后timer将会停止工作.

获取当前event

本项目中将不同的使用节次虚拟化为不同的event.

只有在eventregister才有权限上传图片.

只能在event开始前12h到event结束后1天的时间范围内上传图片. 如果event结束, timer也不会再更新.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//get the corresponded event
var event = app.globalData.event;
var participants = event.participants;
var is_signed = false;
for (var i = 0; i < participants.length; i++) {
if (app.globalData.user.openid == participants[i].openid) {
//matching current user
//check whether the event is expired, -12h to 1d
var can_upload = ((((Date.now() - event.precise_time) / 86400000) >= 1) || (((Date.now() - event.precise_time) / 86400000) <= -0.5)) ? false : true;
//fill the basic event name info and decide whether the uploader should be shown or not
this.setData({
event: event,
all_snapshots_tip: "查看" + event.name + "的全部图片",
is_uploader_hide: !can_upload,
tip_footer: "请在活动开始前12小时到活动结束后1天内上传图片"
})
if (((Date.now() - event.precise_time) / 86400000) >= 1) {
clearInterval(timer);
this.setData({
tip_footer: "活动已结束",
is_dynamic_data_hide: true
})
//event expired, clear interval
}
is_signed = true;
break;
}
//cannot upload if unsigned
if (!is_signed) {
this.setData({
event: event,
all_snapshots_tip: "查看" + event.name + "的全部图片",
is_uploader_hide: true,
tip_footer: "未报名活动,无法上传图片"
})
}
}

从数据库中获取当前event所对应的图片表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
db.collection("events").where({
_id: event._id
}).field({
_id: true,
snapshots: true
}).get({
success: function (res) {
//all shots taken in the event
var snapshots = res.data[0].snapshots
that.setData({
event_id: res.data[0]._id,
event_shots: snapshots
})
var markers = [];
markers[0] = {
id: 0,
latitude: that.data.latitude,
longitude: that.data.longitude,
width: 20,
height: 20,
iconPath: "image/star.png",
callout: {
content: that.data.event.device.name + ":" + that.data.event.device.deviceid,
bgColor: "#fff",
padding: "5px",
borderRadius: "5px",
borderWidth: "1px",
borderColor: "#1296DB",
display: "BYCLICK",
fontSize: "10",
},
is_snapshot: false
};
for (var i = 0; i < snapshots.length; i++) {
var snapshot = snapshots[i];
var marker = {};
//geopoint need to be transformed to json
var location = snapshot.location.toJSON().coordinates;
marker.id = i + 1;
marker.location = snapshot.location;
marker.longitude = location[0];
marker.latitude = location[1];
marker.openid = snapshot.openid;
marker.name = snapshot.name;
marker.avatar = snapshot.avatar;
marker.nickname = snapshot.nickname;
marker.realname = snapshot.realname;
marker.taker = snapshot.nickname + " (" + snapshot.realname + ")";
marker.detail = snapshot.detail;
marker.iconPath = "image/imagepoint.png";
marker.url = snapshot.url;
marker.width = 20;
marker.height = 20;
marker.callout = {
content: snapshot.detail,
bgColor: "#fff",
padding: "5px",
borderRadius: "5px",
borderWidth: "1px",
borderColor: "#1485EF",
display: "ALWAYS",
fontSize: "10",
};
marker.is_snapshot = true;
markers.push(marker);
//deviation, prevent markers from overlapping
}
for (var i = 0; i < markers.length; i++) {
var marker = markers[i];
for (var j = 0; j < markers.length; j++) {
if (markers[j].location == marker.location) {
markers[j].longitude += i * 0.0002;
markers[j].latitude += i * 0.0002;
}
}
}
console.log(markers);
that.setData({
markers: markers
})
wx.hideLoading({
complete: (res) => {},
})
}
})

当前定位点和图片定位点都需要用markers[]盛放, 为了避免渲染冲突, 我们将当前定位点存在markers[0]中, 并对其单独赋值.

如果有坐标相同的定位点, 为了防止它们重合, 这里单独做了特判, 将每一对中重合的标记点其中的一个平移.

从OneNet获取设备信息

wgs84togcj02函数在上文有提及.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//get gps datapoints
get_datapoints: function () {
var that = this;
return new Promise((resolve, reject) => {
wx.request({
url: `https://api.heclouds.com/devices/${that.data.event.device.deviceid}/datapoints?datastream_id=Latitude,Logitude,Speed&limit=1`,
header: {
'content-type': 'application/json',
'api-key': that.data.event.device.apikey
},
success: (res) => {
const status = res.statusCode;
const response = res.data;
var speed = response.data.datastreams[0].datapoints;
var longitude = response.data.datastreams[1].datapoints;
var latitude = response.data.datastreams[2].datapoints;
var current_sp = Number(speed[speed.length - 1].value);
var current_lo = Number(longitude[longitude.length - 1].value);
var current_la = Number(latitude[latitude.length - 1].value);
//encrypt to gcj to fit Tencent map
const encrypt_res = wgs84togcj02(current_la, current_lo);
that.setData({
speed: current_sp,
longitude: encrypt_res.longitude,
latitude: encrypt_res.latitude,
})
console.log("[onenet][speed]: " + that.data.speed);
console.log("[onenet][latitude]: " + that.data.latitude);
console.log("[onenet][longitude]: " + that.data.longitude);
if (status !== 200) {
reject(res.data)
return;
}
if (response.errno !== 0) {
reject(response.error)
return;
}
if (response.data.datastreams.length === 0) {
reject("No data yet.")
}
resolve({})
},
fail: (err) => {
reject(err)
}
})
})
},

使用Promise取消异步.

在地图上点击图片点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//marker tapped
show_snapshots: function (e) {
var id = e.detail.markerId;
var markers = this.data.markers;
if (id == 0) return;
for (var i = 1; i < this.data.markers.length; i++) {
markers[i].iconPath = "image/imagepoint.png";
markers[i].callout.borderColor = "#1485EF";
//change imagepoint to red
if (i == id) {
markers[i].iconPath = "image/imagepoint_selected.png";
markers[i].callout.borderColor = "#EF2914";
}
}
var current_marker = markers[id];
this.setData({
markers: markers,
current_marker: current_marker,
is_image_previewer_hide: false
})
},

显示所有图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//show all tab tapped
show_all_snapshots: function () {
//no snapshot
var event = this.data.event;
if (!this.data.event.snapshots_count) {
this.setData({
all_snapshots_tip: "暂无图片"
})
return;
}
//has snapshots
if (this.data.is_all_Hide) {
this.setData({
is_all_Hide: false,
all_snapshots_tip: "收起"
})
} else {
this.setData({
is_all_Hide: true,
all_snapshots_tip: "查看" + this.data.event.name + "全部图片"
})
}
},

选择上传位点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
choose_location: function () {
var that = this;
wx.chooseLocation({
latitude: that.data.latitude,
longitude: that.data.longitude,
complete: (res) => {
if (!res.name) {
this.setData({
tip: "未选中位置,点我重新选择"
})
} else {
this.setData({
tip: res.name
})
}
this.data.snapshots.avatar = app.globalData.user.avatar;
this.data.snapshots.openid = app.globalData.user.openid;
this.data.snapshots.nickname = app.globalData.user.nickname;
this.data.snapshots.realname = app.globalData.user.realname;
this.data.snapshots.name = res.name;
this.data.snapshots.location = db.Geo.Point(res.longitude, res.latitude);
var d = new Date();
this.data.snapshots.time = d.getTime();
if (this.data.detail) {
this.data.snapshots.detail = this.data.detail;
} else {
this.data.snapshots.detail = "暂无描述";
}
},
})
},

选择要上传的图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//the rider choose an image from snapshots just taken
choose_image: function () {
if (this.data.files.length >= 1) {
wx.showToast({
title: '每位用户单个地点最多上传一张图片',
icon: "none"
})
return;
}
var that = this;
wx.chooseImage({
//choose compressd image to get faster upload and save data
sizeType: ['original', 'compressed'],
count: 1,
//take a snapshot or choose a photo
sourceType: ['album', 'camera'],
success: function (res) {
//check the size of the image
var maxsize = 4000000;
if (res.tempFiles[0].size > maxsize) {
var original_size = (res.tempFiles[0].size / 1000000).toFixed(2);
wx.showToast({
title: '图片过大(' + original_size + 'MB' + '),请取消勾选"原图"或另行上传较小的图片',
icon: 'none'
})
return;
}
//return file path and attach to page data filepath array
that.setData({
files: that.data.files.concat(res.tempFilePaths),
is_upload_add_hide: true,
tip_second: "长按图片删除"
});
}
})
this.choose_location();
},

经过测试, 如果图片大小超过4Mib可能会造成微信闪退, 故加入了图片大小检测, 以提升系统的健壮性.

上传图片和备注

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
//upload image with location and detail
upload_images: function () {
var that = this;
//check if the user is uploading another snapshot to the same point
for (var i = 0; i < this.data.markers.length; i++) {
var marker = this.data.markers[i];
if (marker.openid == app.globalData.openid && marker.name == this.data.snapshots.name) {
wx.showToast({
icon: 'none',
title: '每位用户单个地点最多上传一张图片',
})
return;
}
}
//check if the location is specified
if (!this.data.snapshots.name) {
wx.showToast({
icon: 'none',
title: '未选择位置',
})
return;
}
//check if the detail of snapshot is provided
if (!this.data.detail) {
wx.showModal({
title: '提示',
content: '是否填写图片备注?',
cancelColor: 'gray',
cancelText: '否',
confirmText: '是',
complete: function (e) {
if (e.cancel) {
wx.showModal({
title: '提示',
content: '确认上传该照片到' + that.data.snapshots.name + "?",
cancelText: '取消',
confirmText: '确认',
success: function (res) {
if (res.cancel) {
return;
} else {
that.upload_image_final();
}
}
})
} else {
return;
}
}
})
} else {
wx.showModal({
title: '提示',
content: '确认上传该照片到' + that.data.snapshots.name + "?",
cancelText: '取消',
confirmText: '确认',
success: function (res) {
if (res.cancel) {
return;
} else {
that.upload_image_final();
}
}
})
}
},

上传数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
upload_image_final: function () {
wx.showNavigationBarLoading({
complete: (res) => {},
})
wx.showToast({
title: '图片上传中',
icon: 'loading',
duration: 5000
})
var that = this;
const filePath = that.data.files[0];
//const filePath = files[i];
//use this when uploading mutiple details
const cloudPath = `events/${that.data.event.name}/${app.globalData.user.nickname}/${app.globalData.openid}_${Math.random()}_${Date.now()}.${filePath.match(/\.(\w+)$/)[1]}`;
wx.cloud.uploadFile({
cloudPath,
filePath,
success: function (res) {
files_cloud_url = res.fileID;
that.setData({
files_cloud_url: files_cloud_url
})
var snapshots = that.data.snapshots;
//regenerate detail
if (that.data.detail != "暂无描述" && that.data.detail != "") {
snapshots.detail = that.data.detail;
} else {
snapshots.detail = "暂无描述";
}
console.log(snapshots);
//add url field
snapshots.url = that.data.files_cloud_url;
var e = that.data.event_shots;
e.push(snapshots);
var snapshots_count = e.length;
that.setData({
event_shots: e
})
console.log(that.data.event_shots);
wx.cloud.callFunction({
name: 'update_snapshots_count',
data: {
taskId: that.data.event_id.toString(),
my_snapshot_count: snapshots_count
}
})
wx.cloud.callFunction({
name: 'update_snapshots',
data: {
taskId: that.data.event_id.toString(),
my_snapshot: that.data.event_shots,
}
}).then(res => {
wx.hideNavigationBarLoading({
complete: (res) => {},
})
wx.showToast({
title: '上传成功',
duration: 3000,
success(res) {
wx.pageScrollTo({
scrollTop: 0,
})
setTimeout(that.onLoad, 3000);
}
})
})
}
})
}

对于图像的云储存, 我们使用将文件上传到存储区并维护链接表及链接表个数统计的方案.

下文的代码为云上文件存储路径的命名规则, 以及存储区的文件组织架构

1
const cloudPath = `events/${that.data.event.name}/${app.globalData.user.nickname}/${app.globalData.openid}_${Math.random()}_${Date.now()}.${filePath.match(/\.(\w+)$/)[1]}`;

数据库中相关数据存储架构 /event.name/
数据库中相关数据存储架构 /event.name/user.nickname/

cloudFunction update_snapshots_count

update_snapshots_countupdate_snapshots用来更新数据库中图片counter及图片表.

数据库中相关内容

1
2
3
4
5
6
7
8
9
exports.main = async (event, context) => {
return await new Promise((resolve, reject) => {
db.collection('events').doc(event.taskId).update({
data: {
snapshots_count: event.my_snapshot_count
}
})
}).then(console.log("[update_snapshots][updated successfully]"))
}

cloudFunction update_snapshots

1
2
3
4
5
6
7
8
9
exports.main = async (event, context) => {
return await new Promise((resolve, reject) => {
db.collection('events').doc(event.taskId).update({
data: {
snapshots: event.my_snapshot
}
})
}).then(console.log("[update_snapshots][updated successfully]"))
}

项目展示

警告: 由于小程序基本功能需要后台支持 请不要下载源码自行编译体验.

提示: 文档中内容只有主要功能部分 如需详细讲解请联系.


团队进度

开发进度

项目 开发进度 迭代进度
Arduino 100% 100%
微信小程序 100% 100%

组员进度

请注意对照火花空间相关内容.

成员 团队分工 开发进度 贡献率
ElliasK 所有模块的初始开发、迭代开发和调试 100% 90%
Picaloe 设备调试协力、影视协力、文案协力 100% 10%

统计数据

硬件端

GitHub repo size GitHub commit activity

软件端

GitHub repo size GitHub commit activity

调试日志

2020/11/16 校内 户外调试

机型: v0.0-pre 原型机前体 HYAsst THE ORIGIN de pre-release

调试项目调试结果
GPS精度测试通过(WG-84标准)
WG-84GCJ-02黑箱结果测试基本拟合, 部分点位有10m以上的精度误差
基础便携性测试通过
系统稳定性测试 (ESP8266方案)通过
ESP8266通信能力测试通过, 全时连接

2020/11/18 寝室 黑箱算法结论验证

测试项目测试结果
WG-84GCJ-02黑箱结果测试 (二周目)部分点位漂得厉害. 厄厄, 太野蛮了

2020/11/20 居庸关 长途奔袭调试

机型: v0.0-pre 原型机前体 HYAsst THE ORIGIN de pre-release

调试项目调试结果
ESP8266移动中通信能力测试通过, 全时连接
高速便携性测试通过

Photos

发车前
居庸关
居庸关

2020/11/22 蟒山 户外调试

机型: v0.0-pre 原型机前体 HYAsst THE ORIGIN de pre-release

调试项目调试结果
ESP8266移动中通信能力测试通过, 全时连接
高速便携性测试通过
恶劣路面便携性测试未通过, 有杜邦线松动
系统稳定性测试 (ESP8266方案) (二周目)检测到的死机一次, 原因不明
骑行中陀螺仪数据采集success

Photos

蟒山脚下合照

2020/11/25-27 寝室 室内续航测试

机型: v0.1 初代机 ASKR YGGDRASILLS

调试项目调试结果
ESP8266方案续航测试 (900mAh)113min, 续航良好
系统稳定性测试 (ESP8266方案) (三周目)检测到的死机一次, 原因为ESP8266TCP释放失败
SIM800C方案续航测试 (900mAh)185min, 续航良好
系统稳定性测试 (SIM800C方案)在进行TCP通信时触发事故告警会死机, 需要断电重启.
  • 理想最大电源solution: 6500mAh, 可提供最大电源solution: 30Ah
  • 奇怪的事情发生了, SIM800C续航比ESP8266还长(未曾设想的道路).

[FIXED] SIM800C在进行TCP通信时触发事故告警会死机

2020/11/28 校内 户外调试

机型: v0.1 初代机 ASKR YGGDRASILLS

从此次开始所有的系统稳定性测试默认为SIM800C方案.

调试项目调试结果
SIM800C通信能力测试通过, 全时连接
精确的WG-84 to GCJ-02公式验证通过
GPS速度精度测试通过, 测得误差约为±0.092kmph
事故侦测算法及参数验证 (二周目)通过, 判断准确
基础便携性测试 (二周目)通过
高速便携性测试 (二周目)通过
恶劣路面便携性测试 (二周目)通过
系统稳定性测试 (二周目)通过

[FIXED] 微信小程序中当前点标注由于图片中心点问题在画布缩小后看起来定位不准

[FIXED] timer = setInterval失效 只能手动刷新

Photos

调试工具人: 移动电脑架
桶上调试

2020/12/06 校内 户外调试

机型: 三代目: RAMIEL OCTAHEDRON

调试项目调试结果
事故侦测算法及参数验证 (三周目)通过, 判断准确
高速便携性测试 (三周目)通过
恶劣路面便携性测试 (三周目)通过
系统稳定性测试通过
整体通信能力测试通过, 全时连接
续航测试 (900mAh) (二周目)203min, 续航良好
系统稳定性测试 (三周目)通过

开发点滴

音乐推荐

[梶浦由記] the Garden of sinners -劇場版 空の境界 音楽集-

[London Symphony Orchestra] 交響組曲 機動戦士ガンダムSEED DESTINY Symphony SEED DESTINY

电子熏香

2020年11月20日早晨6点25分, 由于没睡醒进行了一些奇妙操作导致SIM800C冒烟. R.I.P.

去世的SIM800C

从此我组给部分硬件损毁的情况起了别名:

别名 实际故障
放炮 炸电解电容/二极管
电子熏香 板子糊了
核爆 炸电源

支離滅裂な思考と発言

Apple punch

🍎砸牛顿, 于是有了万有引力定律.

🍎砸我们, 于是弯了电容和杜邦线.

事故现场

🍎惨剧
🍎惨剧

团队故事

Picaloe

首先非常有幸能与我们小组组长一同完成本次大作业,本人是一个完全技术上的萌新小白,但与一个拥有过硬技术的组长共同完成了这个我认为很完美的项目,过程中我体会到了完成一个项目的难度,技术掌握,项目规划,文案策划等等,组长很早就已经开始策划本次大作业的方向了,由于本人技术原因,在技术创新方面我出力甚少,但在文案策划排版方面,我学到了许多新知识,例如LaTeXhtml一些的文本代码的编写与格式的实现。我想说,这个项目,无论在,硬件,软件,还是文案布局方面,都付出了极大的心血,组长打代码的声音我是忘不了了。这次项目下来,我认识到了小组合作的重要性,也初步加深了对这个专业作为工科的理解,希望今后能慢慢从一只萌新小白成长为一位老打工人。

ElliasK



这是一次很充实、很有趣、很具有挑战性、自认为不管是过程还是效果都姑且认为可以令人满意的大作业。

首先,在项目初期元认知和项目初期思维层面就具有一定的挑战。我们从用户的角度出发,积极运用课内所体悟到的元认知的相关内容,在分析用户需求、确定产品定位等方面深刻体悟到了元认知对于项目初期项目确立的重要意义。我们不但应用了课内所学的元认知的内容,还对课内的知识进行了进一步的扩展,在本次项目的实践中,发现我最初的胡思乱想是有效果的,是对实践有指导意义的。这个创新便是Q&A部分(在次文档的开头有这一部分的简单举例)。我们通过一对一的用户访谈以及针对于产品功能及技术实现的方案公开招标,收集到了大量热心用户(特别是鸿雁自行车协会中的具有丰富骑行经验的热心车友)的质疑和建议,在分析与解答热心用户的提问或建议的同时,我们发现了项目中所存在的,不管是功能方面的还是技术实现方面的漏洞,并在项目进行的过程中进行调整,使得我们的项目尽可能的完善。我们很希望这种思路可以进入信息与通信工程导论课程元认知教学的内容中,旨在增强需求分析能力、促进研发交流、提升社区活力,为更多优秀作品的诞生垫下基石。

得益于项目初期科学理论的指导以及热心用户的帮助,我自认为我们的项目具有创新性与实用性。创新性无需多言:面向广大骑行组织以及个人的,能够结合实时监测、事故侦测、信息共享的,便于携带且造价低廉的,端到端的,软硬结合的骑行安全监控系统是史无前例的。对于实用性,通勤时马路交通的复杂性、竞赛或日常训练高速骑行的危险性是众所周知的,以地图为画板进行图片共享是将图片与位置结合的最好手段,实时定位以及小程序中所开发的其他功能项目对于自行车协会运营的简化程度也是可想而知的。正因如此,可以说,这个项目充分发挥了硬件–>云<–>微信小程序的端到端系统的功能优势,在创新性与实用性的方面做到了能够真正解决实际问题、开辟新的道路的程度。

其次,在项目的研发阶段,我们应用了项目管理、科技创新管理的知识理论体系,在PMBOKPRINCEⅡ等专业化项目管理理论体系,有效地做到了一下几点指标:

  1. 人尽其力。 我们的小组成员实力参差不齐,我作为组长,自认为拥有较强的组织领带和项目经理能力,并且不管是基础的代码编写、焊接、视频制作、文档撰写,还是比较高等的算法设计及应用、数据结构设计及应用、元认知、项目开发思维,相比与组员都有得天独厚的优势,所以贡献主要在项目开发和迭代过程中。对于技术力较弱的队友,因其有摄影的爱好,便承担了展示视频制作中的素材采集工作,在开发和调试过程中所担任的后勤保障的工作也极为重要。在开发的过程中,我们分工明确,配合默契,比起我单打独斗,效率的确有显著提升,项目开发体验明显改善,营造了一种清新愉快的和睦气氛。

  2. 指标动态化。 众所周知,项目的目标并不完全是从项目的开始就定好的,由于项目干系人(在此次项目中,项目干系人大多为Q&A体系中的热心用户,特别是车协中心组活动部的成员以及在项目开发方面有深刻见解的学长)需求的不断变化,开发进程中对技术把握的更新,在开发进程中发现的项目的问题的解决,导致项目的目标及技术实现等指标不断变化。在诸多变化之中,我们积极应对,应用DDP(动态动态规划)的思想,在每一次状态转移中都尽力保持能够达到最终结果的最优解的状态,从而在各种层面上保证了产品的完整性和完成度。

  3. 物尽其用。 得益于科技创新理论体系的指引,我们在对技术手段的选取上可谓是毫不冗余,物尽其用。我们对应于每一个需求所采取的技术手段都是经过深思熟虑的,使得功能实现的技术开销能够维持在较低的水平。例如,我们之所以要做端到端系统,并不是像某些同年级项目一样只是为了得到更高的项目复杂度,进而得到更高的分数,我们采取端到端的理由是因为端到端系统真的是我们的刚需,是我们无法回避的。如果我们的项目可以不使用端到端系统,能够使用更简单的、实现代价更低的系统就可以完成,并且其性能没有过大损失的话,我们舍弃端到端便是理所应当之事。对于WG-84 to GCJ-02制式转换,我们最开始采取模拟退火(莫得深度)进行黑箱测试,可以得到基本满足使用精度的可行解;但是经过后来的测试,我们发现在一些奇怪的点位上我们得出的解有明显的偏移,于是我们果断使用数学方法,最终得出了精准的解决方案。对于事故侦测中对一定时间内采集到的数据的均值的维护,采取了被广泛认同是最佳方案的循环数组的方案,能够在尽可能节省空间的前提下实现功能。对于数据库内容的更新,我们直接采取了云函数的方式。尽可能使用较为简单便捷的技术方案,不管是在开发过程中的代价问题,还是在使用过程中的资源占用以及健壮性问题,尽量使用简单便捷的技术方案是有其现实意义的,是项目开发过程中所必须追求的。这是项目思维的体现,是对技术的精准掌握的体现,是对项目本身指标熟悉程度的体现,是对用户的尊重的体现,是项目工程师或项目经理对自身技术能力以及元认知能力的极佳彰显。

第三,在迭代过程中,有了前两步的积累(特别是Q&A模块产生的反馈),我们在实现基础功能之后,在迭代过程中应该选取的方向已经很明确了。对于一个在移动场景中使用的产品,我们充分注重了产品的便携性(包括形状,大小,重量,坚固程度)以及续航,虽然便携性结果并不太令人满意,由于时间的限制我们预测我们的印刷电路板无法及时到货,我们所有的设备(除了集信息显示、电源接入、电路稳定、线材收束、开关控制等功能为一体的我花了将近两天晚上来设计和焊接的大小仅有4x6cm的我也不知道到底应该叫什么的洞洞板之外)基本停留在公版方案上,虽然使用铜柱将所有板子固定,并且将杂乱无章的线材收束起来,再用大排线接到arduino板上(上文提到的我也不知道到底应该叫什么的洞洞板其实是双面焊接,背面也有排针(焊得我险些去世(不是))(硬件连接中有展示图片),整个产品的便携性也是可以保证的。在续航方面,我们取得了在常规工况下长达23小时的优秀的续航成绩,以至于我们因此取消了本来打算实装的睡眠方案,改而采取优化发包频率的策略。正是由于我们在前期充分重视元认知和用户反馈,我们在迭代时有充足的抓手,能够快且精准地找到方向,使迭代过程顺利进行。

最后,在项目收尾过程(包括项目文档撰写、展示视频制作等)中,由于前期项目进行过程中的良好习惯,收尾进程也进行地尤为顺利。在此次项目开发的过程中,我充分意识到了一个完全的项目的最基本要求其中的几条就是元认知、项目开发思维、项目管理思维、科技创新管理思维的始终贯彻,良好开发习惯的始终保持。感谢这次大作业让我在大学的第一个学期里拥有了一段愉快的、难以忘怀的记忆,进一步促进了我对项目开发的热爱。这依然成为我记忆宝库中为数不多的珍宝,将伴随着我度过从今以后的岁月。

室外调试过程中拍的照片中的几张

十三陵水库
居庸关
京藏高速

Next Dream.. @2020.12.07

进一步迭代前景展望

  1. 性能更强功耗更低成本更低的MCU. 目前计划使用STM32进行进一步开发.
  2. 更好的通信功耗及性能. 目前计划改用NB-IoT模块, 使用CoAP协议.
  3. 更美的外观, 更高的便携性, 更好的电气性能. 计划绘制并印刷电路板, 完成各项设备的集成化.
  4. 更好的软件优化. 重构硬件代码, 码风更为清秀、封装性更强的同时充分考虑各种使用场景, 力求性能和功耗平衡的最优解.
  5. 微信小程序代码性能进一步优化, 注重代码性能和效率.
  6. 继续完善微信小程序周边功能, 比如用户注册时的短信验证码, 个人信息修改等等.

Dream never eclipses. We are always on the way forward.

Recerence

TheWayForward

FAIOJ C++参考手册

已知不可逆的坐标转换算法,通过结果进行反算的方法

w3school ECMAScript 6 Promise对象

GCJ-02到WG-84坐标变换

百度文库 NEO-7_ProductSummary_(GPS.G7-HW-11003)

KalmanFilterforDummies

Introduction-and-implementations-of-the-kalman-filter

百度文库 基于单片机控制的积分式电压表设计

SCoop Github issue about timer0_overflow_count

Arduino PROGMEM

SIM800 Series AT Command Manual V1.07

百度文库 SIM800C_硬件设计手册_V1.01

CSDN JOY_TECH SIM800C的使用心得

百度文库 MPU6050 技术手册

微信小程序文档 - 微信开放文档

Arduino tutorials

ArduinoISP

The LaTeX Project

CTEX : OnlineDocuments LaTeX

Hypertext Help with LaTeX

LATEX2e 插图指南

编译原理——链接过程

OneNET 文档中心

OneNET 多协议接入

程序员大本营 HTTP报文格式

知乎 HTTP报文格式简介

std::string::c_str

WeUI 文档

GitHub Tencent WeUI

Parser富文本插件 文档

GitHub jin-yufeng Parser

Comments

Please contact the Administrator directly for emergency.





GitHub release (latest by date including pre-releases) GitHub release (latest by date including pre-releases) GitHub repo size GitHub repo size PictureBed PictureBed

Blog content follows the Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) License

Copyright 2017-2021 ELLIAS views, viewersLoading... Loading...
MOE ICP 辽ICP备20009666号-1 | MOE ICP MOEICP备20211096号