2015年11月15日星期日

[自己动手玩黑科技] 1、小黑科技——如何将普通的家电改造成可以与手机App联动的“智能硬件” - beautifulzzzz

本邮件内容由第三方提供,如果您不想继续收到该邮件,可 点此退订
[自己动手玩黑科技] 1、小黑科技――如何将普通的家电改造成可以与手机App联动的"智能硬件" - beautifulzzzz  阅读原文»

NOW,5 将此黑科技传授予你~

一、普通家电控制电路板分析

普通家电其人机接口一般由按键和指示灯组成(高端的会稍微复杂,这里不考虑)

这样交互过程,其实就是:由当前指示灯信息,按照操作流程按相应按键,来实现相应功能的过程:

  

注:每次按动相应的按键都会导致相应的指示灯自身的状态从0到1或者从1到0变化,这其实是一个最好的反馈

那么,我们将一个完整的过程定义为:按动某个按键(或几个,同时或依次),等待某个指示灯呈现某种状态,得到基于特定电器的固有功能(操作)

例如:你按动电热水壶的烧水按钮,此时烧水指示灯变亮,等到烧水指示灯变暗,表明该烧水过程完成

这样,我们可以把传统电器抽象成一个黑盒,你输入一些按钮控制信号,等待特定指示灯返回信号,再做出另外一些控制信号,依次类推,直到得到某个最终反馈信号,表明你通过这一交互过程完成了对黑盒的交互实现了黑盒给定的功能。

因此想要做个手机控制的家电就要用硬件在人与机之间插一脚~

呵呵:从上图来看电水壶已经不纯了,硬件插入到人壶之间后,还把壶卖给了手机,手机又偷偷地把壶和人卖给了服务器~

二、如何设计硬件电路读取指示灯的工作状态和控制开关按下

综上,硬件部分关键在于读灯和按键!

我本来以为:

  ① 一般的传统家电的指示灯电路是在,3.3V~5V左右的电压下工作的,分为高电平点亮和低电平点亮

  ②一般的传统家电的按键按下会产生一个低电平\高电平传入MCU进行去抖等判断

所以原初认为这样就能搞定问题了:

注:通过在电器外部插入一个MCU用来监听内部按键按下和LED管脚的状态来获取,并且可以伪造按键按下信息和LED状态

而实际上是:

  ① 没有所谓的高低电平,得从数字电路的01中跳出来,高电平是大于某个阈值的电平,低电平是低于某个阈值的电平

  ② 按键存在一些特殊情况,比如多路复用(下面是我遇到的情况:就不能仅仅看为高低电平来处理)

注:图中采用3个按钮复用一个MCU输入,通过串联不同阻值的电阻来使不同按键按下后MCU按键输入引脚产生不同的电压,MCU采用一定方式区分出这些电压就能区分出是哪个按键按下

因此:

想获得指示灯状态可以采用——读取指示灯引脚上的电压值,然后在嵌入式程序里计算,如果高于高电平阈值被认为是高电平,如果低于低电平阈值被认为是低电平。

要想模拟按键按下可以采用——在每个按键上并一路继电器开关,插入监听的MCU通过控制继电器模拟按键按下。

如下,是我针对某一款家用电器的改造方案:

其中:

  该家电一共有5个指示灯(图中左上),左1和左2状态互补,其他三个灯状态独立,每个灯与家电内部的MCU都有相应的独立线路相连。由于,上面分析我们不能简单地以数字信号来获取指示灯的状态,所以这里分别利用外置监听MCU的PA0,PA1,PA2和PA3四个引脚来做AD输入读取左1,左3,左4和左5四个指示灯引脚两端的电压值。

  经反复测量分析得到关于指示灯的部分工作信息(图中左下):在家电开机的时候高电平维持在3.28~3.29V左右,此时指示灯是关的(也就是低电平灯亮);低电平维持在0.6V一下,此时指示灯是开着的。特殊情况:在机器断电情况下,PA0~PA3四路AD输入电压在1.5~1.8V之间。

  因此,我将1V作为低电平的上限,将3.2V作为高电平的下线,1~3.2V处于浮空状态。

  对于模拟按键按下则通过3个继电器,每个继电器并联在一个按键之上。如图中间偏上CONTROL WIRE,其中RED WIRE连接在三个按键的公共输入端,BLACK WIRE、WRITE WIRE和GREEN WIRE分别连到相应按钮的另一端。而这四个线又连接到右上角所示的电路上。该电路是一个包含ULN2003和3个继电器的电路。

  因此,只要将VCC和GND加上电压,外部监听MCU用3个引脚连接到IN5\IN6\IN7,若MCU置相应引脚为高则相当于相应按键按下,置低相当于抬起。人的手动按按键的过程相当于按下+延时+抬起的过程。

三、编写嵌入式代码实现4路AD读取指示灯电压值+3路继电器开关控制

上面说到,用继电器模拟按键按下,其实就是设置相应的管脚为:

  高电平+延时一段时间+低电平(高电平表示开始按下,延时表示按下的一个短暂的时间,低电平表示又抬起来了)

其相应的代码大家也能明白:(解析TxBuffer1[0]里的内容,为'a'则让PA4对应的继电器模拟按键按下,为'b'则让PA5对应的继电器模拟按键按下,依次类推...

1 switch(TxBuffer1[0]){
2 case 'a':
3 GPIO_SetBits(GPIOA, GPIO_Pin_4);//置PA4引脚为高
4 ticks = 900000;//延时
5 while(ticks--);
6 GPIO_ResetBits(GPIOA, GPIO_Pin_4);//置PA4引脚为低
7 break;
8 case 'b':
9 GPIO_SetBits(GPIOA, GPIO_Pin_5);
10 ticks = 900000;
11 while(ticks--);
12Java游戏服务器-Netty自动重连与会话管理 - Metazion  阅读原文»

网游少不了网络通信,不像写C++时自己造轮子,Java服务器使用Netty。Netty做了很多工作,使编写网络程序变得轻松简单。灵活利用这些基础设施,以实现我们的需求。

其中一个需求是自动重连。自动重连有两种应用场景:

  • 开始连接时,对端尚未开启
  • 连接中途断开

在有多个服务器(比如LoginServer和GameServer等)时,这样就不用考虑服务器启动顺序。
有需求就需要有解决方案,其实很简单,Netty已经提供,如下:

ctx.channel().eventLoop().schedule(() -> tryConnect(), reconnectInterval, TimeUnit.SECONDS);

tryConnect是实际执行连接的方法,后面两个参数表示每隔reconnectInterval秒重连一次即执行tryConnect,而对应上述两种应用场景的分别是connect失败和channel inactive时,详见后面代码。

自动重连解决后,还有一个问题是如何管理连接。Netty使用Channel来抽象一个连接,但实际开发时,通常逻辑上会有一个会话(Session)对象用来表示对端,可以在其上添加各种逻辑属性方法等,以及收发网络消息。这样一个Channel就需要对应一个Session,且方便互相索引。

首先考虑如何创建这个Session。

为了方便Netty使用和复用,我抽象了一个TcpServer/TcpClient类分别表示服务器和客户端。理想情况是 TcpServer和TcpClient合并为一个,不同行为由Session来决定。但因为Netty的服务器和客户端分别使用ServerBootstrap和Bootstrap,其分别包含bind和connect,这个想法未能实现。

Session有两种,ListenSession负责监听连接请求,TransmitSession负责传输数据。在实际应用中,有这么一种需求,比如GameServer主动连接LoginServer,这时GameServer即作为client端。在连接成功时,需要GameServer主动发个注册消息给LoginServer,LoginServer籍此得知是哪个服务器组。此时,GameServer可能同时会以Client身份连接另一个服务器比如Gateway而且同样要发消息。那么作为client端主动连接的TransmitSession最好细化,需要包含要连接的主机地址、端口和重连时间等信息,也需要在Active时发送不同消息,而Server端TransmitSession并不需要。所以设计上TransmitSession又分为ClientSession和ServerSession。SeverSession由TcpServer在建立连接时自动创建,而ListenSession和ClientSession则由使用者自行创建并交由TcpServer/TcpClient管理。

接口如下:

public abstract class ListenSession {
private boolean working = false;
private int localPort = 0;
private int relistenInterval = 10;
...
public abstract ServerSession createServerSession();
}

public abstract class TransmitSession {
protected Channel channel = null;
protected boolean working = false;
...
public abstract void onActive() throws Exception;
public abstract void onInactive() throws Exception;
public abstract void onException() throws Exception;
public abstract void onReceive(Object data) throws Exception;
public abstract void send(Object data);
}

public abstract class ClientSession extends TransmitSession {
private String remoteHost = "";
private int remotePort = 0;
private int reconnectInterval = 10;
...
}

其次考虑如何管理Channel和Session的对应关系。除了使用一个类似HashMap\的容器来管理外,一个更自然的想法是直接把Session记录在Channel上就好了,这样就省去了每次查找的开销。Netty已经提供了,即Channel的AttrMap。这里或许有疑问的是,client端connect成功的Channel和Active/Inactive时的Channel是同一个,所以可以方便放置/取出数据。而server端bind成功的Channel放置的是ListenSession,Active/Inactive时的Channel却是一个新的,并非bind的Channel,怎么取出之前放入的ListenSession来呢?Netty也想到了,所以提供了Channel.parent()方法,每一个Active时的Channel是由bind时的Channel创建的,后者就是前者的parent。

综上,TcpServer示例如下:

public class TcpServer {
private final AttributeKey<ListenSession> LISTENSESSIONKEY = AttributeKey.valueOf("LISTENSESSIONKEY");
private final AttributeKey<ServerSession> SERVERSESSIONKEY = AttributeKey.valueOf("SERVERSESSIONKEY");

private final ServerBootstrap bootstrap = new ServerBootstrap();
private EventLoopGroup bossGroup = null;
private EventLoopGroup workerGroup = null;

private ArrayList<ListenSession> listenSessions = new ArrayList<ListenSession>();
...
private void start() {
bossGroup = new NioEventLoopGroup(1);
workerGroup = new NioEventLoopGroup(4);
bootstrap.group(bossGroup, workerGroup);
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("encode", new ObjectEncoder());
pipeline.addLast("decode", new ObjectDecoder(ClassResolvers.cacheDisabled(null)));
pipeline.addLast(workerGroup, new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ListenSession listenSession = ctx.channel().parent().attr(LISTENSESSIONKEY).get();
ServerSession serverSession = listenSession.createServerSession();
ctx.channel().attr(SERVERSESSIONKEY).set(serverSession);
serverSession.setChannel(ctx.channel());
serverSession.onActive();
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
ServerSession serverSession = ctx.channel().attr(SERVERSESSIONKEY).get();
serverSession.onInactive();
}
...
}
...
private void tryListen(ListenSession listenSession) {
if (!listenSession.isWorking()) {
return;
}

final int port = listenSession.getLocalPort();
final int interval = listenSession.getRelistenInterval();

ChannelFuture f = bootstrap.bind(port);
f.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture f) throws Exception {
if (f.isSuccess()) {
f.channel().attr(LISTENSESSIONKEY).set(listenSession);
} else {
f.channel().eventLoop().schedule(() -> tryListen(listenSession), interval, TimeUnit.SECONDS);
}
}
});
}
}

如果监听失败则隔interval秒重试,新连接建立时创建ServerSession关联该Channel。
TcpClient的实现大同小异,不同点在于需要在Channel Inactive时执行重连:

public class TcpClient {
private final AttributeKey<ClientSession> SESSIONKEY = AttributeKey.valueOf("SESSIONKEY");

private final Bootstrap bootstrap = new Bootstrap();
private EventLoopGroup workerGroup = null;

private ArrayList<ClientSession> clientSessions = new ArrayList<ClientSession>();
...
private void start() {
workerGroup = new NioEventLoopGroup();
bootstrap.group(workerGroup);
bootstrap.channel(NioSocketChannel.class);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("encode", new ObjectEncoder());
pipeline.addLast("decode", new ObjectDecoder(ClassResolvers.cacheDisabled(null)));
pipeline.addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ClientSession clientSession = ctx.channel().attr(SESSIONKEY).get();
clientSession.setChannel(ctx.channel());
clientSession.onActive();
}

@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
ClientSession clientSession = ctx.channel().attr(SESSIONKEY).get();
clientSession.onInactive();

final int interval = clientSession.getReconnectInterval();
ctx.channel().eventLoop().schedule(() -> tryConnect(clientSession), interval, TimeUnit.SECONDS);
}
...
}
...
private void tryConnect(ClientSession clientSession) {
if (!clientSession.isWorking()) {
return;
}

final String host = clientSession.getRemoteHost();
final int port = clientSession.getRemotePort();
final int interval = clientSession.getReconnectInterval();

ChannelFuture future = bootstrap.connect(new InetSocketAddress(host, port));
future.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture f) throws Exception {
if (f.isSuccess()) {
f.channel().attr(SESSIONKEY).set(clientSession);
} else {
f.channel().eventLoop().schedule(() -> tryConnect(clientSession), interval, TimeUnit.SECONDS);
}
}
});
}
}

如果需要监听多个端口或连接多个目的主机,只需要创建多个ClientSession/ListenSession即可。如:

private TcpServer tcpServer = new TcpServer();

private LSServer lsServer = new LSServer();
private LSClient lsClient = new LSClient();

lsServer.setLocalPort(30001);
lsServer.setRelistenInterval(10);
tcpServer.attach(lsServer);

lsClient.setLocalPort(40001);
lsClient.setRelistenInterval(10);
tcpServer.attach(lsClient);

另外值得一提的是网上很多例子,都会在bind端口后,调用如下代码:

f.channel().closeFuture().sync();

这会阻塞当前线程,其实就是在当前线程做main loop。而实际游戏服务器中,通常main线程做逻辑线程,逻辑线程需要自己tick,也就是自定义main loop,我们在其中执行一些每帧更新的逻辑。所以并不需要上面这种方式。

公共库仓库:JMetazion

服务器示例仓库:JGameDemo

新建QQ交流群:330459037


本文链接:Java游戏服务器-Netty自动重连与会话管理,转载请注明。

阅读更多内容

没有评论:

发表评论