我的家庭自动化系统的实现

很长时间以来,我在Habré上阅读了有关家庭自动化系统的文章,我想描述我已经从事了2年多的工作。为了更好地了解我的情况,您需要做一个简短的介绍。

三年前,我和我的家人搬到了一套新的三居室公寓(67.5平方米),尽管从技术上讲公寓当然是旧的-斯大林,这是一栋建于1946年的房子。铝制两线制电线,某些地方用1平方毫米的铜绞线组成。检修工作即将进行,我决定自己动手做,并从彻底更换布线开始。他们购买了700m的电力电缆,用于照明和1.5和2.5平方毫米的插座,双绞线托架,一些用于电视天线的同轴电缆(以防万一)。为什么这么多以及产生了什么-我要一只猫。

我决定立即进行布线,即:没有配电箱,电缆从每个点到屏蔽层(除了插座(可以是2-3点的一组),电缆从极端到屏蔽层,其余通过回路连接)-t。 e。以防万一,从每个开关将其自己的电缆连接到屏蔽层,从每个照明点将其自己的电缆连接到屏蔽层,沿rj-45点对的插座附近。当然,电缆很多。遵循PUE的所有规则将其放置在地板



上的某个地方,例如在托儿所中:某个地方-在天花板上,例如卧室门的视图:



结果,我们将所有电缆都放置在将放置屏蔽的地方的走廊中,并且可以根据需要在它们之间进行切换。即使将来必须更改连接方案,也不会花费太多时间,也不必破坏维修。当然,在所有场合都制作了许多插座-整个公寓总共约90点。

这是实验过程中临时的“防护罩”的样子:



可怕的景象,但一切正常,这种怪物以这种形式生活了几个月。晴朗的一天,星星成功地形成了,而盾牌又经过了故意的决定而重做。在天花板下的摇摇欲坠的楼梯上花了3天(斯大林的天花板在3m内),但这是值得的。此后,遮阳板开始看起来像这样:



和一般视图:



这与屏蔽的最终形式相去甚远,仍然没有足够的RCD,断路器的数量比必要的少3倍,没有脉冲继电器-但是有非断开线,所有电缆都连接到端子,至少具有某种断路器选择性。

既然您对我必须使用的东西有一个大致的了解,那么您就可以开始描述系统的``大脑''了。事不宜迟,我以arduino为基础,尤其是因为我早就购买了2个freeduino板,2个以太网防护罩和一个电动机防护罩。还从中文订购了几个继电器模块,每个模块4个。我还发现,根据Ob上Habré上一篇文章的建议,可以在其中插入特殊弹簧的开关会变成不固定的开关。是时候编写代码了。

总的来说,我与生活中的计算机息息相关。他完成了自己没写过的皮草垫-pascal,c#,c ++,1c,php,javascript-您仍然可以列出很多。我上一次在Web开发领域工作时,我还处理ip-telephony。因此,提出一种算法很简单,但是对于“为您服务”的电子产品,我知道并知道简单的事情,而涉及到更复杂的事物(微控制器,保险丝,触点弹跳)则更加困难。但是不是神在烧锅,眼睛在害怕,但是手在烧。我决定简化一切。我将arduino的大地馈送到开关的输入,将开关的输出连接到arduino的模拟引脚(尽管可以数字化,但这并不重要)。我将arduino的引脚与继电器模块上的引脚相连。从技术上讲,一切准备就绪,当您按下开关时,arduino会在所需引脚上看到LOW值,并在与继电器模块相连的引脚上设置LOW。假设所有从公寓中的点到终端的电缆都已连接到终端,则连接过程不会花费很多时间。
为了应对联系反弹,在互联网上研究了主题之后,选择了Bounce2库。通常,我最初想编写一个通用代码,以便它可以与至少2个开关(至少22个)一起使用。正是这一任务构成了整个算法的基础。好吧,现在从单词到代码。我不会完全上传所有代码;到github的链接将在本文的结尾。从我的角度来看,我只会展示重要的时刻。

因此,声明库和变量:

#include <Bounce2.h>
#include <EEPROM.h>

const byte cnt = 8;
const byte pin_cnt = 19;

int pins[] = {11,12,13,13,14,15,16,17};
int leds[] = {6, 3, 4, 5, 3, 4, 5, 3};

byte init_leds[cnt] ;
byte init_buttons[cnt];
int button_states[cnt];
Bounce debouncers[cnt];
unsigned long buttonPressTimeStamps[cnt];
boolean changed[cnt];

重点是:随灯/开关的数量增加常数并在阵列中注册新的关联就足够了-就是说,将添加新的灯或开关。同时,代码结构允许一个开关一次控制多个灯具。或几个开关立即控制一盏灯。此外,对于每个开关,都会创建其自己的Bounce对象实例以进行反跳动。

在设置功能中,所有阵列均被初始化,而照明设备的状态存储在非易失性存储器中,以便在发生电源故障或重新启动时,一切都与发生故障之前相同。

  for(byte i=0; i<cnt; i=i+1) {
    EEPROM.write(pins[i], 10);
  }

  for(byte i=0; i<cnt; i=i+1) {
    button_states[i] = 0;
    
    byte value = EEPROM.read(leds[i]);
    if(value==11) {
      init_leds[i] = LOW ;
    }else{
      init_leds[i] = HIGH ;
    }
    init_buttons[i] = HIGH;
    buttonPressTimeStamps[i] = 0;
    changed[i] = false;

    debouncers[i] = Bounce();

    pinMode(pins[i], INPUT);
    pinMode(leds[i], OUTPUT);
    
    digitalWrite(pins[i], init_buttons[i]);
    digitalWrite(leds[i], init_leds[i]);
    
    debouncers[i].attach( pins[i] );
    debouncers[i].interval(5);
  }

关于第一个周期,我什么也不会说,我们稍后再讲。最有趣的开始是主体循环的主体。

void loop(){
  for(byte i=0; i<cnt; i=i+1){
    byte dvalue = EEPROM.read(pins[i]);
    if(dvalue!=11) {
      changed[i] = debouncers[i].update();
      
      if ( changed[i] ) {
        int value = debouncers[i].read();
        if ( value == HIGH) {
         button_states[i] = 0;   
        } else {
           if (i > 0 and pins[i] == pins[i-1]) {
             byte prev_value = EEPROM.read(leds[i-1]);
                            
             if(prev_value == 11) {
               digitalWrite(leds[i], LOW );
               EEPROM.write(leds[i], 11);
             }else{
               digitalWrite(leds[i], HIGH);
               EEPROM.write(leds[i], 10);
             }
           } else {               
             byte value = EEPROM.read(leds[i]);
             if(value==11) {
               digitalWrite(leds[i], HIGH );
               EEPROM.write(leds[i], 10);
             }else{
               digitalWrite(leds[i], LOW);
               EEPROM.write(leds[i], 11);
             }
           }
                 
           button_states[i] = 1;
           buttonPressTimeStamps[i] = millis();     
        }
      }
   
      if ( button_states[i] == 1 ) {
        if ( millis() - buttonPressTimeStamps[i] >= 200 ) {
            button_states[i] = 2;
        }
      }
    }
  }

  delay( 10 );
} 

在测试算法的第一个版本时,注意到孩子(我有三个,男孩)真的很喜欢单击开关。因此,有必要能够关闭某些开关,以使控制器不响应它们。最明显的选择是简单地从板上移去必要的引脚,但这是错误的,没有意思。因此,还将标志写入非易失性存储器,以指示开关是否断开。这是使用以下循环初始化的:

  for(byte i=0; i<cnt; i=i+1) {
    EEPROM.write(pins[i], 10);
  }

  ...

并在这里检查:

  for(byte i=0; i<cnt; i=i+1){
    byte dvalue = EEPROM.read(pins[i]);
    if(dvalue!=11) {
    ...

在这个阶段,墙壁上的本地开关已经开始起作用。但是,如果没有界面,智慧家庭将不会变得聪明。由于我正在从事Web开发,因此决定创建一个Web界面。这就是以太网屏蔽派上用场的地方。不幸的是,我找不到使用以太网屏蔽进行远程控制的程序的第一个版本的源。我将尝试搜索备份,也许它们在那里。但是含义是原始的,可耻的。每个控制器都有自己的IP地址。 Web服务器在arduino上上升,该服务器分析GET请求并根据端口号打开或关闭相应的指示灯。 Internet上有很多此类示例。对于Web界面,服务器是在主板上内置英特尔Atom的服务器,已安装Ubuntu Server 14.02,已安装标准的LAMP套件,一个简单的界面被写在那里。所有资源也将在本文的结尾。目前,它看起来像这样:



如您所见,厨房中的一盏灯已打开,并且负责该开关的开关已锁定。管理非常简单-只需单击所需的项目,即可更改其状态。

如果不是一个“但是”,一切都会很棒。以太网防护罩一直挂着。不,来自交换机的本地控制始终像时钟一样工作。但是从Web界面进行的远程控制不断下降。如果管理层工作一天,那就太好了。但是更常见的是,重新启动后仅几个小时,防护罩就挂了。我只是不尝试处理看门狗,但我的董事会不支持它。我在中国订购并在en28j60上用其他盾牌替换了盾牌,它变的更好了,但仍然定期挂断。我向控制器添加了一个继电器模块,通过其常闭触点为arduino板供电,其中一个arduino板以一定的频率拉动了继电器,并切断了电源,然后恢复了该状态-但是,这也并不总是起作用,并且在重启时闪烁点亮,即使是几秒钟,但仍然如此。这是控制器此时的外观:



然后决定完全放弃以太网屏蔽。我开始寻找其他远程管理功能。我试图将arduins直接连接到服务器,并通过Serial.read()/ Serial.print()发送命令-它每隔两次就工作一次,但由于每次从脚本访问它时板都重新启动,因此无法实现稳定性在服务器上。我读到了很多有关此类错误的信息,我只是意识到它与DTR相连,在他们写道可以用其他标志初始化端口的地方,给出了对我不起作用的示例。一段时间后,我碰到一篇有关用USBAsp编程器制作USB-I2C适配器的文章。我决定-为什么不呢?我从中文订购了几个这样的程序员-然后等待。

一周前,我的包裹到达了。一个程序员用i2c微型USB固件刷新,然后我再次坐下来重写代码。此处的协议功能开始出现。当然,服务器是主机,所有Arduino板都是从机。

该代码应解决以下任务:

-报告所请求端口的状态;
-切换请求端口的状态;
-关闭或打开所有灯。

我遇到了一个问题。我可以使用i2c-tools套件中的标准命令与arduino板进行通信。这是一个团队

i2cget -y < > <>



i2cset -y < > <> 0x00 <byte >

从芒的角度来看,您似乎可以传递“值”一词,但这对我没有用,或者我在某个地方弄错了。

问题是,首先,如果需要切换某个端口的状态,则必须首先传递负责此操作的命令的编号,然后再传递端口号。我什至尝试执行此操作,发送2个命令,并在代码中依次收集它们-但是这很丑陋且不合理。

第二个问题-Arduino在收到数据时无法回答某些问题。Wire库有两种方法-从主机接收数据时使用onReceive(),当主机请求数据时使用onRequest()。

所以我这样做:Web服务器有6个命令:

  • 命令“ 1”-获取所有指示灯的状态
  • 命令“ 2”-获取所有开关的阻塞状态
  • 命令“ 5”-打开整个世界
  • 命令“ 6”-关闭整个世界

命令的格式如下:十进制数,其格式为<百(1或2)-切换灯或开关锁> <端口号(从0到99)>;例如105-开关5端口,213-开关13端口锁定。该命令被转换为十六进制并传递给arduino,后者执行逆变换并了解需要执行的操作。

这是服务器端的样子:

  ...
  if ($action == 3)
     $val_hex = dechex(intval($port) + 100);
  else
     $val_hex = dechex(intval($port) + 200);
  
  exec("sudo i2cset -y 7 $addr 0x00 0x$val_hex", $output);
  ...

从arduino方面:

void receiveEvent(int howMany) {
  byte bytes = Wire.available();
  int x = 0;
  for (byte i=1; i <= bytes; i=i+1) {
    x = Wire.read();
  }
  
  if (x == 1 or x == 2 or x == 5 or x == 6) {
    do_action(x, 0);
  } else {
    if ( x > 200) {
      do_action (4, x - 200);
    } else {
      do_action (3, x - 100);
    }
  }  
}

它看起来很原始,但是可以工作。解决第二个问题如下。这是do_action函数:

void do_action(byte command, byte port) {
  byte value = 0;
  byte dvalue = 0;
  switch (command) {
    case 1:
      start_request = true;
      request_type = 1;
      current_port = 0;
      
      break;
    case 2:
      start_request = true;
      request_type = 2;
      current_port = 0;
      
      break;
    case 3:
      value = EEPROM.read(port);
      if(value==11) {
        digitalWrite(port, HIGH);
        EEPROM.write(port, 10);
      } else {
        digitalWrite(port, LOW);
        EEPROM.write(port, 11);
      }
      
      break;
    case 4:
      dvalue = EEPROM.read(port);
      if(dvalue==11) {
        EEPROM.write(port, 10);
      } else {
        EEPROM.write(port, 11);
      }
      
      break;
    case 5:
      for (byte i=0; i<cnt; i = i + 1) {
        digitalWrite(leds[i], LOW);
        EEPROM.write(leds[i], 11);
      }
      
      break;
    case 6:
      for (byte i=0; i<cnt; i = i + 1) {
        digitalWrite(leds[i], HIGH);
        EEPROM.write(leds[i], 10);
      }
      
      break;
    default:
      
    break;
  }
}

对于第3-6队,一切都很明确,但是第1或第2队可以更详细地描述。服务器首先发送所需的命令,并且当arduino收到命令1或2时,将初始化标志:

  start_request = true;
  request_type = 1;
  current_port = 0;

然后,服务器开始向arduino发送请求的次数与其要轮询的端口的次数相同。在服务器端:

function get_data($address, $action, $cnt) {
	exec("sudo i2cset -y 7 $address 0x00 0x0$action", $output);
	$tmp = @$output[0];
	while (strpos($tmp,"rror")!==false) {
		exec("sudo i2cset -y 7 $address 0x00 0x0$action", $output);
		$tmp = @$output[0];
	}
	$str = "";
	for ($i = 1; $i <= $cnt; $i++) {
		exec("sudo i2cget -y 7 $address", $output);
		$tmp = @$output[0];
		while (strpos($tmp,"rror")!==false) {
			exec("sudo i2cget -y 7 $address", $output);
			$tmp = @$output[0];
		}
		if ($tmp) {
			if (strpos($tmp,"1")!==false)
				$str .= "1";
			else
				$str .= "0";
		}
		unset($output);
		unset($tmp);
	}
			
	return $str;
}

$str = array();
$c = 1;
while ($c <= $tryes) {
	$tmp = get_data($addr, $action, $cnt);
	
	if (strlen($tmp) == $cnt)
		$str[] = $tmp;
	
	$c++;
}

$new_array = array_count_values($str);

asort($new_array);

$res = "";
$max = 0;
foreach ($new_array AS $key=>$val) {
	if ($val >= $max) {
		$res = $key;
		$max = $val;
	}
}

return preg_split('//', $res, -1, PREG_SPLIT_NO_EMPTY);

简而言之-我们进行了几次($ tryes> 3)次尝试来询问arduino,我们得到了一条由0或1组成的行。我们可以在任何地方用任何命令查看答案,如果有单词Error的话-这意味着传输过程中存在错误,您需要重复传输。为了确保传输的字符串的正确性,我们进行了多次尝试,使用array_count_values($ str)方法按字符串折叠数组; 最后,我们得到一个包含相同行出现次数的数组,我们给出了从arduino获得的大部分行。

从arduino的角度来看,一切都更加简单:

void requestEvent() {
  if (request_type == 1) {
    byte value = EEPROM.read(leds[current_port]);
    if(value==11) {
      Wire.write(1);
    } else {
      Wire.write(0);
    }
    
    current_port = current_port + 1;
  } else if (request_type == 2) {
    byte dvalue = EEPROM.read(pins[current_port]);
    if(dvalue==11) {
      Wire.write(1);
    } else {
      Wire.write(0);
    }

    current_port = current_port + 1;
  }
}

该网页的代码包含以下控件:

<a class="lamp living_room" id="lamp0x4d3" rel='0x4d' onclick="lamp_click('0x4d',this.id, 3);" ></a>
<a class="lamp kitchen" id="lamp0x4d4" rel='0x4d' onclick="lamp_click('0x4d',this.id, 4);" ></a>
<a class="lamp children_main" id="lamp0x4d5" rel='0x4d' onclick="lamp_click('0x4d',this.id, 5);" ></a>
<a class="lamp children_second" id="lamp0x4d6" rel='0x4d' onclick="lamp_click('0x4d',this.id, 6);" ></a>
<a class="lamp sleeproom_main" id="lamp0x423" rel='0x42' onclick="lamp_click('0x42',this.id, 3);" ></a>
<a class="lamp sleeproom_lyuda" id="lamp0x424" rel='0x42' onclick="lamp_click('0x42',this.id, 4);" ></a>
<a class="lamp sleeproom_anton" id="lamp0x425" rel='0x42' onclick="lamp_click('0x42',this.id, 5);" ></a>

<a class="button button_living_room" id="button0x4d15" onclick="button_click('0x4d',this.id, 15);" ></a>
<a class="button button_kitchen" id="button0x4d14" onclick="button_click('0x4d',this.id, 14);" ></a>
<a class="button button_children_main" id="button0x4d16" onclick="button_click('0x4d',this.id, 16);" ></a>
<a class="button button_children_second" id="button0x4d17" onclick="button_click('0x4d',this.id, 17);" ></a>
<a class="button button_sleeproom_door1" id="button0x4212" onclick="button_click('0x42',this.id, 12);" ></a>
<a class="button button_sleeproom_door2" id="button0x4213" onclick="button_click('0x42',this.id, 13);" ></a>
<a class="button button_sleeproom_lyuda1" id="button0x4214" onclick="button_click('0x42',this.id, 14);" ></a>
<a class="button button_sleeproom_lyuda2" id="button0x4215" onclick="button_click('0x42',this.id, 15);" ></a>
<a class="button button_sleeproom_anton1" id="button0x4216" onclick="button_click('0x42',this.id, 16);" ></a>
<a class="button button_sleeproom_anton2" id="button0x4217" onclick="button_click('0x42',this.id, 17);" ></a>

实际上,我明确指出了我需要访问的板卡地址和端口号。

哦,是的,控制器现在看起来像这样:



一切都安装到位后,一切都在第一次启动。第二天,出现了难以理解的效果-如果打开卧室中的所有灯,卧室中的所有灯每2-3秒开始闪烁一次。我花了一整夜的时间挑选代码;测试台上没有这样的门框,所以问题不在代码中。我在多个论坛中翻阅,在其中一个论坛的toga中,我找到了类似症状的描述,检查了我的猜测-问题消失了。关键是我用计算机电源(12V)给所有三个arduins供电,而旧的freeduino安静地进食并且没有嗡嗡声,而arduino uno v3却不能,并且在打开所有继电器时,功率调节器被加热了,以便这是不可能的。将电源电压降低到5V 2A-并可以正常工作。

有很多计划,我们需要完成公寓,走廊和带卫生间的浴室的维修,在我的梦里我想控制热水器和每个插座,因为现在我可以在I2C总线上挂任何数量的arduin,并且每个人都可以做自己的事。还计划在DIN导轨上增加脉冲继电器,以便继电器模块仅控制这些脉冲继电器,并且整个负载已经通过了后者。因为对中国继电器模块的可靠性存在严重怀疑。但这就是将来的一切。

如承诺的那样,链接到github:arduino的 Sketchy

Web界面

Source: https://habr.com/ru/post/zh-CN386457/


All Articles