A Thermostat on an Insecure Network Premise: I currently can't wire things up in such a way to make the sensor/control/actuator network separate from everything else. Part of it will need to be wireless as wll. Given that constraint, how to make it "secure enough" such that the needed connectivity can be provided by a network on which I do not control all the code. Entry: Embedded systems: thermostat transport Date: Thu Jan 11 19:39:42 EST 2018 I've been fretting about this a lot. I will need one uC board per remote thermostat to implement USB interface, and one to make the actuator work. For now however, it is possible to skip the thermostats and do only the actuator. How do I construct a secure channel on the uC? What about doing this without security first? Just build the damn relay board. EDIT: Sat. Tired. EDIT: Sun. Why is this so difficult? It is all interconnect and protocols. A lot, for such a small task of flipping a switch. Fundamentally, I cannot accept that communication is so expensive. Ethernet and USB host pretty much require a Linux host. While what I want is something much more low level: a field bus that is easy to implement on a uC. Maybe that is the real challenge here? Anyway, to get it to work, all the elements are there: - USB thermometers + temperv14 - Some program running on the host with the USB thermometer, sending events over network. - A bridge from Ethernet -> USB - A STM32F103 device with USB -> relay board - Relay board connected by pig tail Eventually, I want a shared RS485 bus running some multidrop protocol, probably in master mode with one connection to the ethernet segment. So let's cut it short: - Beaglebone black connected to 5V relay board: cut out the middle man. Maybe later, make an RS485 fieldbus. - Ethernet cable running into BBB - Erlang on BBB GPIO? Ok first problem is 5V/3V. I believe it is this one, but I have seen two with different polarity. https://www.sainsmart.com/products/4-channel-5v-relay-module http://wiki.sunfounder.cc/index.php?title=4_Channel_5V_Relay_Module Assuming the latter is the circuit, it has two LEDs in parallel on a 1K. Might be the reason why 3V3 doesn't switch it, because of the forward voltage. It seems to work on 3V3. 5V: off 3V3: off 0V: on It pulls 1.7mA to ground. It glitches on startup. https://groups.google.com/forum/#!topic/beagleboard/Kq2ftmVdugk cd /sys/class/gpio echo '48' >export cd gpio48 echo '1' >value echo 'out' >direction Then switch with value '0' = engaged '1' = open So I have furnace.sh init/on/off. Next: glitch-free boot Boot is not the problem. It's the power-on. My guess is that 3V3 comes up just a little later than 5V, which is enough to cause the glitch. It might not be a real problem though. Bridging the LEDs might be enough to make it possible to drive this thing from 3V3. Then separating the VCC from JD-VCC. Forward voltage 1.25V. Actually I can measure this. LED 1.8V OPto 1.1V Beagle pins: https://groups.google.com/forum/#!topic/beaglebone/dosKmEE7xso source 4-6 mA sink 8mA Measured sink at 1mA. Alright. Time to hook it up. So. Needs dedicated 100Mbit. Port free? eth1 is not used currently, so maybe use that one? Alternatively, string the actuator wire? Too many degrees of freedom to work with... Let's just fix some. But... connecting it to a different power supply changed the bootup relay current. Is this a problem? The real solution is to run one side on 3V3 compleltely. Take the LED out of the loop and tie in to the opto directly. Next: - find out good activation current - pick resistor (3.3 - 1.1 = 2.2V) - decouple VCC - solder directly on opto Calculation is pretty much the same as for 5V + LED (+-1.7V) 1.7mA measured means currently there is (/ 2.2 1.7) around 1k Optocoupler https://www.vishay.com/docs/83522/k817p.pdf temper-poll -f The rest is software. Needed: - temperature logs - snm stuck? Jan 14 23:34:44 60.8 Jan 15 00:00:53 64.3 Jan 15 00:01:53 64.3 Jan 15 00:02:54 63.7 Jan 15 00:03:54 63.4 Jan 15 00:04:55 63.3 Jan 15 00:05:55 62.9 Jan 15 00:06:55 62.6 Jan 15 00:07:56 62.5 Jan 15 00:08:56 62.2 Jan 15 00:09:56 61.9 Jan 15 00:10:57 61.5 Jan 15 00:11:57 60.8 Jan 15 00:12:57 60.8 Jan 15 00:13:58 60.8 Something is wrong here... Maybe switch to celcius? No it's just not very accurate. Needs calib. Calib against reference Honeywell 588227058599663 C=58, T1=62.3 -4 (broom) C=69, T2=72.1 -3 (zoe) Some design constraints to make this robust. Safety first: - If connection to temperature sensor is lost, shut down the furnace. - If the system crashes for any reason, shut down the furnace. This should be possible to manage using Erlang supervisors, by shutting down the furnace on startup, and crashing whenever any of these exceptions occur. The control loop aside from exceptions is very simple. loop({temp, T}, #{ relay := true, setpoint := {_,Max}} = State) -> Active = T < Max, maps:put(relay, Active, State); loop({temp, T}, #{ relay := false, setpoint := {Min,_}} = State) -> Active = T < Min, maps:put(relay, Active, State). In a first iteration, use only a single sensor to drive. Later, perform the "wire or" in software. As for security, some approaches - HMAC + time stamps - OpenVPN + Erlang protocol - SSH to socket There is no "simple" solution, so let's stick to the one that is easy to do initially, then straightforward to generalize: distributed erlang + openvpn. The VPN host can be the actuator box. For now, reuse one of the existing VPNs. Next: get measurment from remote host into actuator host. It seems simpler to do this by initiating from the actuator host. A ssh connection with fixed command would suffice. root@beaglebone:~/.ssh# ssh -i ./temper broom 20180115-163754 63.6 Ok now everything is there. For now, run it as root to get it working. - Create skeleton rebar app - Use port tool from erl_tools to get temperature messages Entry: Secure sensors Date: Sat Jan 20 09:47:28 EST 2018 Basically, encryption and authentication are needed on the USB device. Don't trust the routers. Maybe encryption isn't necessary, but authentication is. http://ieeexplore.ieee.org/document/7417118/?reload=true Entry: Debug access Date: Sun Jan 21 11:57:51 EST 2018 Could have a hardware safety: flip a switch to turn on ssh. Entry: Simplest possible setup Date: Sun Jan 21 12:54:40 EST 2018 - no authentication: use UDP broadcast for temperature sensors - get the algorithm working for tonight Extensions: - mac + timestamp Entry: temper calibration Date: Wed Jan 31 18:23:04 EST 2018 On 7-port with switches. Ports are left-to-right order 62: root@zoo:/i/tom/git/temper-python# temper-poll -p Found 7 devices Device #0 (bus 1 - port 9.1.4.4): 18.7°C 65.6°F Device #1 (bus 1 - port 9.1.4.3): 17.7°C 63.8°F Device #2 (bus 1 - port 9.1.4.2): 19.1°C 66.3°F Device #3 (bus 1 - port 9.1.4.1): 18.8°C 65.9°F Device #4 (bus 1 - port 9.1.3): 20.4°C 68.7°F Device #5 (bus 1 - port 9.1.2): 18.3°C 65.0°F Device #6 (bus 1 - port 9.1.1): 20.7°C 69.2°F I did a couple. Going to take that one for now: 59: root@zoo:/i/tom/git/temper-python# temper-poll Found 7 devices Device #0: 17.6°C 63.6°F Device #1: 16.7°C 62.0°F Device #2: 17.8°C 64.1°F Device #3: 16.8°C 62.2°F Device #4: 19.1°C 66.3°F Device #5: 17.2°C 62.9°F Device #6: 19.8°C 67.6°F 56: root@zoo:~# temper-poll Found 6 devices Device #0: 15.6°C 60.1°F Device #1: 14.8°C 58.7°F Device #2: 16.2°C 61.2°F Device #3: 15.9°C 60.6°F Device #4: 17.1°C 62.8°F Device #5: 14.8°C 58.5°F 56: root@zoo:~# temper-poll Found 6 devices Device #0: 15.4°C 59.8°F Device #1: 14.4°C 58.0°F Device #2: 16.0°C 60.8°F Device #3: 15.6°C 60.1°F Device #4: 17.1°C 62.7°F Device #5: 14.5°C 58.1°F 57: root@zoo:~# temper-poll Found 7 devices Device #0: 16.2°C 61.2°F Device #1: 15.3°C 59.6°F Device #2: 16.7°C 62.0°F Device #3: 16.2°C 61.2°F Device #4: 17.7°C 63.8°F Device #5: 15.2°C 59.5°F Device #6: 16.7°C 62.0°F %% {RefHoneyWell, Temper1To7} [{62, [65.6, 63.8, 66.3, 65.9, 68.7, 65.0, 69.2]}, {59, [63.6, 62.0, 64.1, 62.2, 66.3, 62.9, 67.6]}, {56, [60.1, 58.7, 61.2, 60.6, 62.8, 58.5]}, {56, [59.8, 58.0, 60.8, 60.1, 62.7, 58.1]}] C= [{62, [65.6, 63.8, 66.3, 65.9, 68.7, 65.0]}, {59, [63.6, 62.0, 64.1, 62.2, 66.3, 62.9]}, {56, [60.1, 58.7, 61.2, 60.6, 62.8, 58.5]}, {56, [59.8, 58.0, 60.8, 60.1, 62.7, 58.1]}]. (gwtest_tom@panda.i)9> [[round(10*(T-T0)) || T <- Ts] || {_T0,[T0|_]=Ts} <- C]. [[0,-18,7,3,31,-6], [0,-16,5,-14,27,-7], [0,-14,11,5,27,-16], [0,-18,10,3,29,-17]] (gwtest_tom@panda.i)10> [[round(10*(T-T0)) || T <- Ts] || {T0,Ts} <- C]. [[36,18,43,39,67,30], [46,30,51,32,73,39], [41,27,52,46,68,25], [38,20,48,41,67,21]] What's a good way to compute this? The reference is very low resolution, so average it out. Entry: Finish up Date: Wed Jan 31 19:30:22 EST 2018 - Thermometer in every room. Have these send UDP to beaglebone. Don't bother with auth at this point. - Temper devices are associated to PC - Put calib data in beaglebone - Make a web app with bootstrap so it works well on the phone. No security for now. Next: udev -> poller -> UDP Maybe best done as a small C libusb application to not pull in too many dependencies. Modify temper14.c? Entry: Client connection Date: Thu Feb 1 17:45:57 EST 2018 It really doesn't matter: - ssh with key - udp Entry: UDP receiver Date: Thu Feb 1 19:02:35 EST 2018 Set up an UDP receiver in Erlang. I'm going to likerly need this some other time, so just go ahead and figure out how to do it. It's actually very simple: root@zoo:~# socat exec:temper-poll UDP4-DATAGRAM:10.1.3.97:2001 root@beaglebone:~# erl Erlang/OTP 19 [erts-8.2.1] [source] [async-threads:10] [kernel-poll:false] Eshell V8.2.1 (abort with ^G) 1> gen_udp:open(2001). {ok,#Port<0.224>} 2> receive X -> X end. {udp,#Port<0.224>, {10,1,3,2}, 39591, "Found 7 devices\nDevice #0: 16.4°C 61.6°F\nDevice #1: 15.4°C 59.7°F\nDevice #2: 16.9°C 62.5°F\nDevice #3: 16.6°C 61.9°F\nDevice #4: 17.9°C 64.3°F\nDevice #5: 15.8°C 60.4°F\nDevice #6: 18.1°C 64.6°F\n"} Entry: Cleaned up temperv14.c Date: Thu Feb 1 22:37:36 EST 2018 Next is to have it send UDP. OK done. Next is to add it to udev. Entry: UDP reconnects dont't work properly Date: Fri Feb 2 11:03:30 EST 2018 f(Pid), Pid = serv:start({handler,fun()-> #{ port => gen_udp:open(2001) } end, fun(Msg,State) -> io:format("~p~n",[Msg]),State end}). tom@beaglebone:~/erl_tools$ ./erl.sh Erlang/OTP 19 [erts-8.2.1] [source] [async-threads:10] [kernel-poll:false] Eshell V8.2.1 (abort with ^G) 1> f(Pid), Pid = serv:start({handler,fun()-> #{ port => gen_udp:open(2001) } end, fun(Msg,State) -> io:format("~p~n",[Msg]),State end}). <0.59.0> {udp,#Port<0.335>,{10,1,3,2},57594,[112,16]} {udp,#Port<0.335>,{10,1,3,2},57594,[112,16]} {udp,#Port<0.335>,{10,1,3,2},57594,[112,16]} root@zoo:/i/tom/git/temperv14# ./temperv14.elf 1070 16.44 61.59 1070 16.44 61.59 1070 16.44 61.59 Works now. Next: load temperv14.elf automatically from udev. OK Entry: temperv14.erl restart on error Date: Fri Feb 2 12:31:21 EST 2018 Ok, got it. main() is a restart loop calling the old main() as start() 15> {udp,#Port<0.351>,{10,1,3,23},59787,[112,21]} {udp,#Port<0.351>,{10,1,3,24},44439,[32,18]} {udp,#Port<0.351>,{10,1,3,2},51547,[48,17]} {udp,#Port<0.351>,{10,1,3,12},46086,[16,22]} Entry: Thermostat app Date: Fri Feb 2 13:09:04 EST 2018 Create this as a complete application, with supervisor that initiates a safe shutdown. Entry: Inspecting temperatures Date: Fri Feb 2 15:36:31 EST 2018 Just following one cycle of the honeywell thermostat. {dev,23} is living room. Furnace switches on: 254> obj:dump(thermostat). #{dev => 23, last => 6954, ping => <0.164.0>, socket => #Port<0.593>, {dev,2} => {6951,16.4375}, {dev,12} => {6954,22.0625}, {dev,23} => {6950,19.0625}, {dev,24} => {6953,16.5625}} Starts blowing: 255> obj:dump(thermostat). #{dev => 23, last => 7044, ping => <0.164.0>, socket => #Port<0.593>, {dev,2} => {7042,16.375}, {dev,12} => {7044,22.0625}, {dev,23} => {7040,18.9375}, {dev,24} => {7043,16.5625}} Switches off: 278> obj:dump(thermostat). #{dev => 23, last => 7433, ping => <0.164.0>, socket => #Port<0.593>, {dev,2} => {7433,16.4375}, {dev,12} => {7430,22.0625}, {dev,23} => {7432,18.875}, {dev,24} => {7429,16.8125}} Fan stops: 288> obj:dump(thermostat). #{dev => 23, last => 7611, ping => <0.164.0>, socket => #Port<0.593>, {dev,2} => {7609,16.4375}, {dev,12} => {7611,22.0625}, {dev,23} => {7607,19.0625}, {dev,24} => {7610,16.875}} A bit after: 299> obj:dump(thermostat). #{dev => 23, last => 7726, ping => <0.164.0>, socket => #Port<0.593>, {dev,2} => {7724,16.4375}, {dev,12} => {7726,22.0625}, {dev,23} => {7723,19.0625}, {dev,24} => {7725,16.8125}} A bit more: 3> obj:dump(thermostat). #{dev => 24, last => 288, ping => <0.64.0>, socket => #Port<0.418>, {dev,2} => {287,16.4375}, {dev,12} => {287,22.0625}, {dev,23} => {288,18.75}, {dev,24} => {286,16.625}} Clicks on again: 4> obj:dump(thermostat). #{dev => 24, last => 419, ping => <0.64.0>, socket => #Port<0.418>, {dev,2} => {417,16.3125}, {dev,12} => {417,22.0625}, {dev,23} => {419,18.6875}, {dev,24} => {416,16.5}} Entry: Control algorithm Date: Fri Feb 2 15:41:51 EST 2018 The Honeywell uses a 1 deg F hysteresis. Mabye use the same? I wonder if the problem is more about thermometer placement. Entry: These thermometers have a large thermal mass Date: Fri Feb 2 16:53:32 EST 2018 Maybe reduce the spread? Best to gather some data mapping input to actuator output. Setting spread to 0.25 C That's enough to get past the noise. It will overshoot substantially due to inertia. Entry: Per IP Date: Sun Feb 4 10:03:13 EST 2018 [{1,zoo}, {2,lroom}, {3,zoe}, {4,beaglebone} {5,garage} {6,broom} {7,groom}]. Entry: CData Date: Sun Feb 4 11:54:04 EST 2018 %% zoo lroom zoe beaglebone garage broom groom Meas = [{62, [65.6, 63.8, 66.3, 65.9, 68.7, 65.0, 69.2]}, {59, [63.6, 62.0, 64.1, 62.2, 66.3, 62.9, 67.6]}, {56, [60.1, 58.7, 61.2, 60.6, 62.8, 58.5]}, {56, [59.8, 58.0, 60.8, 60.1, 62.7, 58.1]}], FCal = lists:foldl( fun({Ref, Temps}, M0) -> lists:foldl( fun({Host, Temp}, M1) -> List = maps:get(Host, M1, []), maps:put(Host, [{Ref,Temp} | List], M1) end, M0, zip2(Hosts,Temps)) end, #{}, Meas), #{garage => [{56,62.7},{56,62.8},{59,66.3},{62,68.7}], broom => [{56,58.1},{56,58.5},{59,62.9},{62,65.0}], beaglebone => [{56,60.1},{56,60.6},{59,62.2},{62,65.9}], groom => [{59,67.6},{62,69.2}], lroom => [{56,58.0},{56,58.7},{59,62.0},{62,63.8}], zoe => [{56,60.8},{56,61.2},{59,64.1},{62,66.3}], zoo => [{56,59.8},{56,60.1},{59,63.6},{62,65.6}]} zip2([],_) -> []; zip2(_,[]) -> []; zip2([A|As],[B|Bs]) -> [{A,B}|zip2(As,Bs)]. %% Edit: I've renamed devices afterward so there might be errors Entry: ice point calibration Date: Sun Feb 4 17:55:34 EST 2018 %% DAC values at ice point + diff to previously calibrated C value {zoo,592} {lroom,192} {broom,160} {zoe,528} {garage,656} {groom,944} {beaglebone,336} Seems to work a lot better than previous approach. Entry: mess Date: Mon Feb 5 11:56:09 EST 2018 Didn't take long before it became a mess. - Confusion about types during refactoring leading to errors - Loosing steam during a refactoring run - Indecisiveness about location/propagation/representation of state Entry: old calib Date: Mon Feb 5 13:36:09 EST 2018 ice point works well, so removing old code %% calib() -> %% %% Honeywell thermostat as reference. %% %% Host => [{Ref,Temp}]. %% %% Data collected as farenheit. %% %% Identified by hostname. %% CalF = #{ %% 18 => [{56,62.7},{56,62.8},{59,66.3},{62,68.7}], %% 24 => [{56,58.1},{56,58.5},{59,62.9},{62,65.0}], %% 97 => [{56,60.1},{56,60.6},{59,62.2},{62,65.9}], %% 23 => [{56,58.0},{56,58.7},{59,62.0},{62,63.8}], %% 12 => [{56,60.8},{56,61.2},{59,64.1},{62,66.3}], %% 2 => [{56,59.8},{56,60.1},{59,63.6},{62,65.6}], %% 19 => [{59,67.6},{62,69.2}] %% far off, possibly bad data, was broken slot7 %% }, %% maps:map( %% fun(_Name, TableF) -> %% %% [{Ref,Temp}] -> Avg %% SumDiffF = lists:foldl( %% fun({RefF,TempF},Acc) -> Acc + RefF - TempF end, %% 0, TableF), %% CorrF = SumDiffF / length(TableF), %% CorrC = CorrF / 1.8, %% %% Same resolution as sensor. %% round(CorrC,16) %% end, %% CalF). %% round(C,N) -> round(C * N) / N. Entry: UI Date: Sat Mar 17 08:57:02 EDT 2018 I'll make the UI depend on the ui application, which will have all the machinery needed to run small widgets. There are some tools issues to resolve. ui does depend on jiffy, which I need to cross-compile. I want to get rid of jiffy. To do that, the javascript code needs to have an eterm encoder. EDIT: Jumped through some hoops to get a staging area for testing the eterm encoder. Entry: Thermostat on exo Date: Sun Mar 18 19:10:51 EDT 2018 Two parallel paths: - get exo to run on gwbbb (needs some network fixes) - create gui to run on exo-tp or some other component Switch between the two when one gets too much. Entry: Made a draft gui Date: Mon Apr 9 11:28:05 EDT 2018 Two things came up: - gui needs to receive events - current state is not displayed When it starts up, set the target to "don't act" ? Entry: Autodetect? Date: Wed Apr 18 10:08:26 EDT 2018 Based on the profile of temperature rise, it is probably possible to detect which vents are open. Maybe have this trigger when one of the temperatures goes way over setpoint?