forked from dracoventions/TWCManager
-
Notifications
You must be signed in to change notification settings - Fork 0
/
TWCManager.pl
executable file
·1975 lines (1800 loc) · 89.5 KB
/
TWCManager.pl
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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/perl
################################################################################
# Perl code and TWC protocol 1 reverse engineering by Chris Dragon.
#
################################################################################
# Modified by vloschiavo 3/18/18 - Added support for Tesla Powerwall stats #
################################################################################
#
# Additional logs and hints provided by Teslamotorsclub.com users:
# TheNoOne, IanAmber, and twc.
# Thank you!
#
# For support and information, please read through this thread:
# https://teslamotorsclub.com/tmc/threads/new-wall-connector-load-sharing-protocol.72830
#
# Report bugs at https://github.com/cdragon/TWCManager/issues
#
# A Python project based on TWCManager is available here:
# https://github.com/wido/smarttwc
#
# This software is released under the "Unlicense" model: http://unlicense.org
# This means source code and TWC protocol knowledge are released to the general
# public free for personal or commercial use. I hope the knowledge will be used
# to increase the use of green energy sources by controlling the time and power
# level of car charging.
#
# WARNING:
# Misuse of the protocol described in this software can direct a Tesla Wall
# Charger to supply more current to a car than the charger wiring was designed
# for. This will trip a circuit breaker or may start a fire in the unlikely
# event that the circuit breaker fails.
# This software was not written or designed with the benefit of information from
# Tesla and there is always a small possibility that some unforeseen aspect of
# its operation could damage a Tesla vehicle or a Tesla Wall Charger. All
# efforts have been made to avoid such damage and this software is in active use
# on the author's own vehicle and TWC.
#
# In short, USE THIS SOFTWARE AT YOUR OWN RISK.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# For more information, please visit http://unlicense.org
################################################################################
# What's TWCManager good for?
#
# This script (TWCManager) pretends to be a Tesla Wall Charger (TWC) set to
# master mode. When wired to the IN or OUT pins of real TWC units set to slave
# mode (rotary switch position F), TWCManager can tell them to limit car
# charging to any whole amp value between 5A and the max rating of the charger.
# Charging can also be stopped so the car goes to sleep.
#
# This level of control is useful for having TWCManager track the real-time
# availability of green energy sources and direct the slave TWCs to use near the
# exact amount of energy available. This saves energy compared to sending the
# green energy off to a battery for later car charging or off to the grid where
# some of it is lost in transmission.
#
# TWCManager can also be set up to only allow charging during certain hours,
# stop charging if a grid overload or "save power day" is detected, reduce
# charging on one TWC when a "more important" one is plugged in, or whatever
# else you might want to do.
#
# One thing TWCManager does not have direct access to is the battery charge
# percentage of each plugged-in car. There are hints on forums that some TWCs
# do report battery state, but we have yet to see a TWC send such a message.
# It's possible the feature exists in TWCs with newer firmware.
# This is unfortunate, but if you own a Tesla vehicle being charged, people have
# figured out how to get its charge state by contacting Tesla's servers using
# the same password you use in the Tesla phone app. Be very careful not to
# expose that password because it allows unlocking and starting the car.
################################################################################
# Vince Loschiavo - I've added support for Tesla Powerwall 2 to monitor
# energy output of your solar system.
################################################################################
################################################################################
# Overview of protocol TWCs use to load share
#
# A TWC set to slave mode (rotary switch position F) sends a linkready message
# every 10 seconds.
# The message contains a unique 4-byte id that identifies that particular slave
# as the sender of the message.
#
# A TWC set to master mode sees a linkready message. In response, it sends a
# heartbeat message containing the slave's 4-byte id as the intended recipient
# of the message.
# The master's 4-byte id is included as the sender of the message.
#
# Slave sees a heartbeat message from master directed to its unique 4-byte id
# and responds with its own heartbeat message containing the master's 4-byte id
# as the intended recipient of the message.
# The slave's 4-byte id is included as the sender of the message.
#
# Master sends a heartbeat to a slave around once per second and expects a
# response heartbeat from the slave.
# Slaves do not send heartbeats without seeing one from a master first. If
# heartbeats stop coming from master, slave resumes sending linkready every 10
# seconds.
# If slaves stop replying to heartbeats from master, master's behavior is more
# complex but it generally keeps trying to contact the slave at less frequent
# intervals and I think it gives up eventually.
#
# Heartbeat messages contain a 7-byte data block used to negotiate the amount of
# power available to each slave and to the master.
# The first byte is a status indicating things like is TWC plugged in, does it
# want power, is there an error, etc.
# Next two bytes indicate the amount of power requested or the amount allowed in
# 0.01 amp increments.
# Next two bytes indicate the amount of power being used to charge the car, also in
# 0.01 amp increments.
# Last two bytes seem to be unused and always contain a value of 0.
use Fcntl;
use POSIX;
use Time::HiRes qw(usleep nanosleep);
use IPC::SysV qw(ftok IPC_PRIVATE IPC_RMID IPC_CREAT MSG_NOERROR IPC_NOWAIT);
use warnings;
use strict;
# This makes print output to screen immediately instead of waiting till
# the end of the line is reached.
$| = 1;
##########################
#
# Configuration parameters
#
# Most users will have only one ttyUSB adapter plugged in and the default value
# of '/dev/ttyUSB0' below will work. If not, run 'dmesg |grep ttyUSB' on the
# command line to find your rs485 adapter and put its ttyUSB# value in the
# parameter below.
# If you're using a non-USB adapter like an RS485 shield, the value may need to
# be something like '/dev/serial0'.
my $rs485Adapter = '/dev/ttyUSB0';
# Set $wiringMaxAmpsAllTWCs to the maximum number of amps your charger wiring
# can handle. I default this to a low 6A which should be safe with the minimum
# standard of wiring in the areas of the world that I'm aware of.
# Most U.S. chargers will be wired to handle at least 40A and sometimes 80A,
# whereas EU chargers will handle at most 32A (using 3 AC lines instead of 2 so
# the total power they deliver is similar).
# Setting $wiringMaxAmpsAllTWCs too high will trip the circuit breaker on your
# charger at best or START A FIRE if the circuit breaker malfunctions.
# Keep in mind that circuit breakers are designed to handle only 80% of their
# max power rating continuously, so if your charger has a 50A circuit breaker,
# put 50 * 0.8 = 40 here.
# 40 amp breaker * 0.8 = 32 here.
# 30 amp breaker * 0.8 = 24 here.
# 100 amp breaker * 0.8 = 80 here.
# IF YOU'RE NOT SURE WHAT TO PUT HERE, ASK THE ELECTRICIAN WHO INSTALLED YOUR
# CHARGER.
my $wiringMaxAmpsAllTWCs = 6;
# If all your chargers share a single circuit breaker, set $wiringMaxAmpsPerTWC
# to the same value as $wiringMaxAmpsAllTWCs.
# Rarely, each TWC will be wired to its own circuit breaker. If you're
# absolutely sure your chargers each have a separate breaker, put the value of
# that breaker * 0.8 here, and put the sum of all breakers * 0.8 as the value of
# $wiringMaxAmpsAllTWCs.
# For example, if you have two TWCs each with a 50A breaker, set
# $wiringMaxAmpsPerTWC = 50 * 0.8 = 40 and $wiringMaxAmpsAllTWCs = 40 + 40 = 80.
my $wiringMaxAmpsPerTWC = 6;
# After determining how much green energy is available for charging, we add
# $greenEnergyAmpsOffset to the value. This is most often given a negative value
# equal to the average amount of power consumed by everything other than car
# charging. For example, if your house uses an average of 2.8A to power
# computers, lights, etc while you expect the car to be charging, set
# $greenEnergyAmpsOffset = -2.8.
#
# If you have solar panels, look at your utility meter while your car charges.
# If it says you're using 0.67kW, that means you should set
# $greenEnergyAmpsOffset = -0.67kW * 1000 / 240V = -2.79A assuming you're on the
# North American 240V grid. In other words, during car charging, you want your
# utility meter to show a value close to 0kW meaning no energy is being sent to
# or from the grid.
my $greenEnergyAmpsOffset = 0;
# Choose how much debugging info to output.
# 0 is no output other than errors.
# 1 is just the most useful info.
# 9 includes raw RS-485 messages transmitted and received
# 10 is all info.
# 11 is more than all info.
my $debugLevel = 1;
# Normally we fake being a TWC Master. Set $fakeMaster = 0 to fake being a TWC
# Slave instead (only useful for debugging and protocol reversing).
my $fakeMaster = 1;
# TWC's rs485 port runs at 9600 baud which has been verified with an
# oscilloscope. Don't change this unless something changes in future hardware.
my $baud = 9600;
# All TWCs ship with a random two-byte ID. We default to using 0x7777 as our
# fake TWC ID. There is a 1 in 64535 chance that this ID will match each real
# TWC on the network, in which case you should pick a different random id below.
# This isn't really too important because even if this ID matches another TWC on
# the network, that TWC will pick its own new random ID as soon as it sees ours
# conflicts.
my $fakeTWCID = "\x77\x77";
# TWCs send a seemingly-random byte after their 2-byte TWC id in a number of
# messages. I call this byte their "Sign" for lack of a better term. The byte
# never changes unless the TWC is reset or power cycled. We use hard-coded
# values for now because I don't know if there are any rules to what values can
# be chosen. I picked 77 because it's easy to recognize when looking at logs.
# These shouldn't need to be changed.
my $masterSign = "\x77";
my $slaveSign = "\x77";
# If enabled, this script will poll your local Tesla Powerwall 2 gateway
# to monitor the current power going to and from the grid.
my $enablePowerwall = 1;
# Define the IP address of your local Powerwall. You may need to do some
# investigation to determine the local IP address of your Powerwall gateway.
# The gateway is the box that monitors and controls the flow of electricity
# between your solar(if applicable), powerwalls, and the grid.
# The gateway also communicates with Tesla's servers to upload statistics
# periodically. We can poll the gateway very frequently to get updated stats
# in JSON format over http. If enabled and configured this script will poll the
# instantaneous grid power and use that as the green energy source.
# Define the IP address of your Powerwall 2 gateway. You can look at your local
# router's DHCP leases and look for one you don't recognize. Alternatively,
# If your subnet was 192.168.100.0/24 (192.168.100.0-192.168.100.255) then I'd
# suggest the following nmap command to narrow down the list. Look for a system
# identified as linux and a MAC address registered to Winsystems. See example
# below:
# $ sudo apt-get update && sudo apt-get install -y nmap
# $ nmap -sS -O -p80 192.168.100.0/24
#
# Sample nmap output:
# Nmap scan report for 192.168.100.100 <------ your IP will likely be different
# Host is up (0.00035s latency).
# PORT STATE SERVICE
# 80/tcp open http
# MAC Address: 00:01:45:07:BE:EF (Winsystems) <--- your mac will be different
# Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
# Device type: general purpose
# Running: Linux 3.X
# OS CPE: cpe:/o:linux:linux_kernel:3
# OS details: Linux 3.11 - 3.14
# Network Distance: 1 hop
# In the above example you should set $powerwallIP = "192.168.100.100";
my $powerwallIP = "192.168.xxx.xxx";
#
# End configuration parameters
#
##############################
my ($data, $dataLen);
my ($msg, $msgLen) = ('', 0);
# Set options to make the $rs485Adapter work correctly, then open it for reading
# and writing.
# 'raw' and '-echo' options are necessary with the FTDI chipset to avoid corrupt
# output or missing data.
system("stty -F $rs485Adapter raw -echo $baud cs8 -cstopb -parenb");
sysopen(TWC, "$rs485Adapter", O_RDWR | O_NONBLOCK) or die "Can't open $rs485Adapter";
binmode TWC, ":raw";
my @slaveHeartbeatData = (0x04,0x00,0x00,0x00,0x19,0x00,0x00);
my $numInitMsgsToSend = 10;
my $msgRxCount = 0;
my $timeLastTx = 0;
my %slaveTWCs;
my @slaveTWCRoundRobin = ();
my $idxSlaveToSendNextHeartbeat = 0;
my $maxAmpsToDivideAmongSlaves = 0;
my $scheduledAmpsMax = 0;
my $scheduledAmpsStartHour = -1;
my $scheduledAmpsEndHour = -1;
my $scheduledAmpsDaysBitmap = 0x7F;
my $spikeAmpsToCancel6ALimit = 16;
my $timeLastGreenEnergyCheck = 0;
my $hourResumeTrackGreenEnergy = -1;
# __FILE__ contains the path to the running script. Replace the script name with
# TWCManagerSettings.txt. This gives us a path that will always locate
# TWCManagerSettings.txt in the same directory as the script even when pwd does
# not match the script directory.
my $settingsFileName = __FILE__ =~ s|/[^/]+$|/TWCManagerSettings.txt|r;
my $nonScheduledAmpsMax = -1;
my $timeLastHeartbeatDebugOutput = 0;
my $webMsgPacked = '';
my $webMsgMaxSize = 300;
my $webMsgResult;
load_settings();
################################################################################
# Create an IPC (Interprocess Communication) message queue that we can
# periodically check to respond to queries from the TWCManager web interface.
#
# These messages will contain commands like "start charging at 10A" or may ask
# for information like "how many amps is the solar array putting out".
#
# The message queue is identified by a numeric key. This script and the web
# interface must both use the same key. The "ftok" function facilitates creating
# such a key based on a shared piece of information that is not likely to
# conflict with keys chosen by any other process in the system.
#
# ftok reads the inode number of the file or directory pointed to by its first
# parameter. This file or dir must already exist and the permissions on it don't
# seem to matter. The inode of a particular file or dir is fairly unique but
# doesn't change often so it makes a decent choice for a key. We use the parent
# directory of the TWCManager script.
#
# The second parameter to ftok is a single byte that adds some additional
# uniqueness and lets you create multiple queues linked to the file or dir in
# the first param. We use 'T' for Tesla.
#
# If you can't get this to work, you can also set $key = <some arbitrary number>
# and in the web interface, use the same arbitrary number. While that could
# conflict with another process, it's very unlikely to.
my $webIPCkey = ftok(__FILE__ =~ s|/[^/]+$|/|r, ord('T'));
# Use the key to create a message queue with read/write access for all users.
my $webIPCqueue = msgget($webIPCkey, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
if(! defined $webIPCqueue) {
die("ERROR: Can't create Interprocess Communication message queue to communicate with web interface.\n");
};
# After the IPC message queue is created, if you type 'sudo ipcs -q' on the
# command like, you should see something like:
# ------ Message Queues --------
# key msqid owner perms used-bytes messages
# 0x5402ed16 491520 pi 666 0 0
#
# Notice that we've created the only IPC message queue in the system. Apparently
# default software on the pi doesn't use IPC or if it does, it creates and
# deletes its message queues quickly.
#
# If you want to get rid of all queues because you created extras accidentally,
# reboot or type 'sudo ipcrm -a msg'. Don't get rid of all queues if you see
# ones you didn't create or you may crash another process.
# Find more details in IPC here:
# http://www.onlamp.com/pub/a/php/2004/05/13/shared_memory.html
################################################################################
printf("TWC Manager starting as fake %s with id %02x%02x and sign %02x\n\n",
($fakeMaster ? "Master" : "Slave"),
vec($fakeTWCID, 0, 8), vec($fakeTWCID, 1, 8),
vec($slaveSign, 0, 8));
for(;;) {
# In this area, we always send a linkready message when we first start.
# Whenever there is no data available from other TWCs to respond to, we'll
# loop back to this point to send another linkready or heartbeat message.
# By only sending our periodic messages when no incoming message data is
# available, we reduce the chance that we will start transmitting a message
# in the middle of an incoming message, which would corrupt both messages.
# Add a sleep to prevent pegging pi's CPU at 100%. Lower CPU means less
# power used and less waste heat.
usleep(500);
if($fakeMaster) {
# A real master sends 5 copies of linkready1 and linkready2 whenever it
# starts up, which we do here.
# It doesn't seem to matter if we send these once per second or once per
# 100ms so I do once per 100ms to get them over with.
if($numInitMsgsToSend > 5) {
send_master_linkready1();
usleep(100); # give slave time to respond
$numInitMsgsToSend--;
}
elsif($numInitMsgsToSend > 0) {
send_master_linkready2();
usleep(100); # give slave time to respond
$numInitMsgsToSend--;
}
else {
# After finishing the 5 startup linkready1 and linkready2 messages,
# master will send a heartbeat message to every slave it's received
# a linkready message from. Do that here.
if(time - $timeLastTx > 0) {
# It's been about a second since our last heartbeat.
if(@slaveTWCRoundRobin > 0) {
my $slaveTWC = $slaveTWCRoundRobin[$idxSlaveToSendNextHeartbeat];
if(time - $slaveTWC->{timeLastRx} > 26) {
# A real master stops sending heartbeats to a slave that
# hasn't responded for ~26 seconds. It may still send
# the slave a heartbeat every once in awhile but we're
# just going to scratch the slave from our little black
# book and add them again if they ever send us a
# linkready.
printf("WARNING: We haven't heard from slave "
. "%02x%02x for over 26 seconds. "
. "Stop sending them heartbeat messages.\n\n",
vec($slaveTWC->{ID}, 0, 8), vec($slaveTWC->{ID}, 1, 8));
delete_slave($slaveTWC->{ID});
}
else {
$slaveTWC->send_master_heartbeat();
}
$idxSlaveToSendNextHeartbeat++;
if($idxSlaveToSendNextHeartbeat >= @slaveTWCRoundRobin) {
$idxSlaveToSendNextHeartbeat = 0;
}
usleep(100); # give slave time to respond
}
}
}
}
else {
# As long as a slave is running, it sends link ready messages every 10
# seconds. They trigger any master on the network to handshake with the
# slave and the master then sends a status update from the slave every
# 1-3 seconds. Master's status updates trigger the slave to send back
# its own status update.
# As long as master has sent a status update within the last 10 seconds,
# slaves don't send link ready.
# I've also verified that masters don't care if we stop sending link
# ready as long as we send status updates in response to master's status
# updates.
if(time - $timeLastTx >= 10) {
if($debugLevel >= 1) {
printf("Advertise fake slave %02x%02x with sign %02x is "
. "ready to link once per 10 seconds as long as master "
. "hasn't sent a heartbeat in the last 10 seconds.\n",
vec($fakeTWCID, 0, 8), vec($fakeTWCID, 1, 8),
vec($slaveSign, 0, 8));
}
send_slave_linkready();
}
}
############################################################################
# See if there's any message from the web interface.
# If the message is longer than $msgMaxSize, MSG_NOERROR tells it to return
# what it can of the message and discard the rest.
# When no message is available, IPC_NOWAIT tells msgrcv to return $msgResult
# = 0 and $! = 42 with description 'No message of desired type'.
# If there is an actual error, $webMsgResult will be -1.
# On success, $webMsgResult is the length of $webMsgPacked.
$webMsgResult = msgrcv($webIPCqueue, $webMsgPacked, $webMsgMaxSize, 2, MSG_NOERROR | IPC_NOWAIT );
if($webMsgResult < 1 && int($!) != 42) {
print(time_now() . ": Error " . int($!) . ": $! with msgrcv result $webMsgResult\n");
}
elsif($webMsgResult > 0) {
my ($webMsgType, $webMsgTime, $webMsgID, $webMsg) = unpack("lLSa*", $webMsgPacked);
if($debugLevel >= 1) {
print time_now() . ": Web query: '" . $webMsg . "', id " . $webMsgID
. ", time " . $webMsgTime . ", type " . $webMsgType
. ", length " . $webMsgResult . "\n";
}
my $webResponseMsg = '';
if($webMsg eq 'getStatus') {
$webResponseMsg =
sprintf("%.2f", $maxAmpsToDivideAmongSlaves)
. '`' . sprintf("%.2f", $wiringMaxAmpsAllTWCs)
. '`' . $nonScheduledAmpsMax
. '`' . $scheduledAmpsMax
. '`' . sprintf("%02d:%02d",
floor($scheduledAmpsStartHour),
floor(($scheduledAmpsStartHour % 1) * 60))
. '`' . sprintf("%02d:%02d",
floor($scheduledAmpsEndHour),
floor(($scheduledAmpsEndHour % 1) * 60))
. '`' . $scheduledAmpsDaysBitmap
. '`' . sprintf("%02d:%02d",
floor($hourResumeTrackGreenEnergy),
floor(($hourResumeTrackGreenEnergy % 1) * 60))
. '`' . @slaveTWCRoundRobin;
for(my $i = 0; $i < @slaveTWCRoundRobin; $i++) {
$webResponseMsg .= '`' . sprintf("%02X%02X",
vec($slaveTWCRoundRobin[$i]->{ID}, 0, 8),
vec($slaveTWCRoundRobin[$i]->{ID}, 1, 8))
. '~' . $slaveTWCRoundRobin[$i]->{maxAmps}
. '~' . sprintf("%.2f", $slaveTWCRoundRobin[$i]->{reportedAmpsActual})
. '~' . $slaveTWCRoundRobin[$i]->{reportedAmpsMax}
. '~' . $slaveTWCRoundRobin[$i]->{reportedState};
}
}
elsif($webMsg =~ m/setNonScheduledAmps=([-0-9]+)/) {
$nonScheduledAmpsMax = $1;
# Save $nonScheduledAmpsMax to SD card so the setting isn't lost on
# power failure or script restart.
save_settings();
}
elsif($webMsg =~ m/setScheduledAmps=([-0-9]+)\nstartTime=([-0-9]+):([0-9]+)\nendTime=([-0-9]+):([0-9]+)\ndays=([0-9]+)/m) {
$scheduledAmpsMax = $1;
$scheduledAmpsStartHour = $2 + ($3 / 60);
$scheduledAmpsEndHour = $4 + ($5 / 60);
$scheduledAmpsDaysBitmap = $6;
save_settings();
}
elsif($webMsg =~ m/setResumeTrackGreenEnergyTime=([-0-9]+):([0-9]+)/m) {
$hourResumeTrackGreenEnergy = $1 + ($2 / 60);
save_settings();
}
elsif($webMsg =~ m/sendTWCMsg=([0-9a-fA-F]+)/) {
my $twcMsg = $1;
$twcMsg =~ s/(..)/chr(hex($1))/ge;
if($fakeMaster) {
main::send_msg($twcMsg);
}
}
if($webResponseMsg ne '') {
if($debugLevel >= 10) {
print(time_now() . ": Web query response: '$webResponseMsg'\n");
}
# In this case, IPC_NOWAIT prevents blocking if the message queue is too
# full for our message to fit. Instead, an error is returned.
if(!msgsnd($webIPCqueue, pack("lLSa*", 1, $webMsgTime, $webMsgID,
$webResponseMsg), IPC_NOWAIT)
) {
print(time_now() . ": Error " . int($!) . ": $! trying to send response to web interface.\n");
}
}
}
############################################################################
# See if there's an incoming message on the RS485 interface.
for(;;) {
$dataLen = sysread(TWC, $data, 1);
if(!defined($dataLen) && $!{EAGAIN}) {
if($msgLen == 0) {
# No message data waiting and we haven't received the start of a
# new message yet. Break out of inner for(;;) to continue at top
# of outer for(;;) loop where we may decide to send a periodic
# message.
last;
}
else {
# No message data waiting but we've received a partial message
# that we should wait to finish receiving.
usleep(10);
next;
}
}
if($dataLen != 1) {
# This should never happen
print("WARNING: No data available.\n");
next;
}
if($msgLen == 0 && ord($data) != 0xc0) {
# We expect to find these non-c0 bytes between messages, so we don't
# print any warning at standard debug levels.
if($debugLevel >= 11) {
printf("Ignoring byte %02x between messages.\n", ord($data));
}
next;
}
elsif($msgLen > 0 && $msgLen < 15 && ord($data) == 0xc0) {
# If you see this when the program is first started, it means we
# started listening in the middle of the TWC sending a message so we
# didn't see the whole message and must discard it. That's
# unavoidable.
# If you see this any other time, it means there was some corruption
# in what we received. It's normal for that to happen every once in
# awhile but there may be a problem such as incorrect termination
# or bias resistors on the rs485 wiring if you see it frequently.
if($debugLevel >= 10) {
printf("Found end of message before full-length message received. Discard and wait for new message.\n", ord($data));
}
vec($msg, 0, 8) = ord($data);
$msgLen = 1;
next;
}
vec($msg, $msgLen, 8) = ord($data);
$msgLen++;
# Messages are usually 17 bytes or longer and end with \xc0\xfe.
# However, when the network lacks termination and bias resistors, the
# last byte (\xfe) may be corrupted or even missing, and you may receive
# additional garbage bytes between messages.
#
# TWCs seem to account for corruption at the end and between messages by
# simply ignoring anything after the final \xc0 in a message, so we use
# the same tactic. If c0 happens to be within the corrupt noise between
# messages, we ignore it by starting a new message whenever we see a c0
# before 15 or more bytes are received.
#
# Uncorrupted messages can be over 17 bytes long when special values are
# "escaped" as two bytes. See notes in send_msg.
#
# To prevent most noise between messages, add a 120ohm "termination"
# resistor in parallel to the D+ and D- lines. Also add a 680ohm "bias"
# resistor between the D+ line and +5V and a second 680ohm "bias"
# resistor between the D- line and ground. See here for more
# information:
# https://www.ni.com/support/serial/resinfo.htm
# http://www.ti.com/lit/an/slyt514/slyt514.pdf
# This explains what happens without "termination" resistors:
# https://e2e.ti.com/blogs_/b/analogwire/archive/2016/07/28/rs-485-basics-when-termination-is-necessary-and-how-to-do-it-properly
if($msgLen >= 16 && ord($data) == 0xc0) {
$msg = unescape_msg($msg, $msgLen);
# Set msgLen = 0 at start so we don't have to do it on errors below.
# length($msg) now contains the unescaped message length.
$msgLen = 0;
$msgRxCount++;
if($debugLevel >= 9) {
print("Rx@" . time_now() . ": " . hex_str($msg) . "\n");
}
if(length($msg) != 16) {
# After unescaping special values and cutting off the trailing
# \xfe that ends an uncorrupted message, the message should
# always be 16 bytes long.
printf("ERROR: Ignoring message of unexpected length %d: %s\n",
length($msg), hex_str($msg));
next;
}
my $checksumExpected = vec($msg, 14, 8);
my $checksum = vec($msg, 2, 8) + vec($msg, 3, 8) + vec($msg, 4, 8)
+ vec($msg, 5, 8) + vec($msg, 6, 8) + vec($msg, 7, 8)
+ vec($msg, 8, 8) + vec($msg, 9, 8) + vec($msg, 10, 8)
+ vec($msg, 11, 8) + vec($msg, 12, 8) + vec($msg, 13, 8);
if(($checksum & 0xFF) != $checksumExpected) {
printf("ERROR: Checksum %x does not match %02x. Ignoring message: %s\n",
$checksum, $checksumExpected, hex_str($msg));
next;
}
if($fakeMaster) {
############################
# Pretend to be a master TWC
if($msg =~ /\xc0\xfd\xe2(..)(.)(..)\x00\x00\x00\x00\x00\x00.\xc0/s) {
# Handle linkready message from slave.
#
# We expect to see one of these before we start sending our
# own heartbeat message to slave.
# Once we start sending our heartbeat to slave once per
# second, it should no longer send these linkready messages.
# If slave doesn't hear master's heartbeat for around 10
# seconds, it sends linkready once per 10 seconds and starts
# flashing its red LED 4 times with the top green light on.
# Red LED stops flashing if we start sending heartbeat
# again.
my $senderID = $1;
my $sign = $2;
my $maxAmps = ((vec($3, 0, 8) << 8) + vec($3, 1, 8)) / 100;
if($debugLevel >= 1) {
printf(time_now() . ": %.2f amp slave TWC %02x%02x is ready to link. Sign: %s\n",
$maxAmps, vec($senderID, 0, 8), vec($senderID, 1, 8),
hex_str($sign));
}
if($maxAmps >= 80) {
# U.S. chargers need a spike to 21A to cancel a 6A
# charging limit imposed in an Oct 2017 Tesla car
# firmware update. See notes where
# $spikeAmpsToCancel6ALimit is used.
$spikeAmpsToCancel6ALimit = 21;
}
else {
# EU chargers need a spike to only 16A. This value
# comes from a forum post and has not been directly
# tested.
$spikeAmpsToCancel6ALimit = 16;
}
if($senderID eq $fakeTWCID) {
print("Slave TWC reports same ID as master: %02x%02x. Slave should resolve by changing its ID.\n");
# I tested sending a linkready to a real master with the
# same ID as master and instead of master sending back
# its heartbeat message, it sent 5 copies of its
# linkready1 and linkready2 messages. Those messages
# will prompt a real slave to pick a new random value
# for its ID.
#
# We mimic that behavior by setting $numInitMsgsToSend =
# 10 to make the idle code at the top of the for(;;)
# loop send 5 copies of linkready1 and linkready2.
$numInitMsgsToSend = 10;
next;
}
# We should always get this linkready message at least once
# and generally no more than once, so this is a good
# opportunity to add the slave to our known pool of slave
# devices.
my $slaveTWC = new_slave($senderID, $maxAmps);
# We expect $maxAmps to be 80 on U.S. chargers and 32 on EU
# chargers. Either way, don't allow
# $slaveTWC->{wiringMaxAmps} to be greater than $maxAmps.
if($slaveTWC->{wiringMaxAmps} > $maxAmps) {
print("\n\n!!! DANGER DANGER !!!\nYou have set \$wiringMaxAmpsPerTWC to "
. $wiringMaxAmpsPerTWC
. " which is greater than the max "
. $maxAmps . " amps your charger says it can handle. "
. "Please review instructions in the source code and consult an "
. "electrician if you don't know what to do.\n\n");
$slaveTWC->{wiringMaxAmps} = $maxAmps / 4;
}
$slaveTWC->send_master_heartbeat();
}
elsif($msg =~ /\xc0\xfd\xe0(..)(..)(.......).\xc0/s) {
# Handle heartbeat message from slave.
#
# These messages come in as a direct response to each
# heartbeat message from master. Slave does not send its
# heartbeat until it gets one from master first.
# A real master sends heartbeat to a slave around once per
# second, so we do the same near the top of this for(;;)
# loop. Thus, we should receive a heartbeat reply from the
# slave around once per second as well.
my $senderID = $1;
my $receiverID = $2;
my @heartbeatData = unpack('C*', $3);
my $slaveTWC = $slaveTWCs{$senderID};
if(!defined($slaveTWC)) {
# Normally, a slave only sends us a heartbeat message if
# we send them ours first, so it's not expected we would
# hear heartbeat from a slave that's not in our list.
printf("ERROR: Received heartbeat message from "
. "slave %02x%02x that we've not met before.\n\n",
vec($senderID, 0, 8), vec($senderID, 1, 8));
next;
}
if($fakeTWCID eq $receiverID) {
$slaveTWC->receive_slave_heartbeat(@heartbeatData);
}
else {
# I've tried different $fakeTWCID values to verify a
# slave will send our $fakeTWCID back to us as
# $receiverID. However, I once saw it send $receiverID =
# 0000.
# I'm not sure why it sent 0000 and it only happened
# once so far, so it could have been corruption in the
# data or an unusual case.
if($debugLevel >= 1) {
printf(time_now() . ": WARNING: Slave TWC %02x%02x status data: %s sent to unknown TWC id %s.\n\n",
vec($senderID, 0, 8), vec($senderID, 1, 8),
hex_ary(@heartbeatData), hex_str($receiverID));
}
}
}
elsif($msg =~ /\xc0\xfd\xeb(..)(..)(.*?)\xc0/s) {
# Handle kWh total and voltage message from slave.
#
# This message can only be generated by TWCs running newer
# firmware. I believe it's only sent as a response to a
# message from Master in this format:
# FB EB <Master ID> <Slave ID> 00 00 00 00 00 00 00 00 00
# Since we never send such a message, I don't expect a slave
# to ever send this message to us, but we handle it just in
# case.
# According to FuzzyLogic, this message has the following
# format on an EU (3-phase) TWC:
# FD EB <Slave ID> 00000038 00E6 00F1 00E8 00
# 00000038 (56) is the total kWh delivered to cars
# by this TWC since its construction.
# 00E6 (230) is voltage on phase A
# 00F1 (241) is voltage on phase B
# 00E8 (232) is voltage on phase C
#
# I'm guessing in world regions with two-phase power that
# this message would be four bytes shorter, but the pattern
# above will match a message of any length that starts with
# FD EB.
my $senderID = $1;
my $receiverID = $2;
my $data = $3;
if($debugLevel >= 1) {
printf(time_now() . ": Slave TWC %02x%02x unexpectedly reported kWh and voltage data: %s.\n\n",
vec($senderID, 0, 8), vec($senderID, 1, 8),
hex_str(substr($data, 0, length($data) - 1)));
}
}
else {
print(time_now() . ": *** UNKNOWN MESSAGE FROM SLAVE:\n" . hex_str($msg)
. "\nPlease private message user CDragon at http://teslamotorsclub.com\n"
. "with a copy of this error.\n\n");
}
}
else {
###########################
# Pretend to be a slave TWC
if($msg =~ /\xc0\xfc\xe1(..)(.)\x00\x00\x00\x00\x00\x00\x00\x00.\xc0/s) {
# Handle linkready1 from master.
# See notes in send_master_linkready1() for details.
my $senderID = $1;
my $sign = $2;
# This message seems to always contain seven 00 bytes in its
# data area. If we ever get this message with non-00 data
# we'll print it as an unexpected message.
if($debugLevel >= 5) {
printf(time_now() . ": Master TWC %02x%02x is cruising the streets. Sign: %ls\n",
vec($senderID, 0, 8), vec($senderID, 1, 8),
hex_str($sign));
}
if($senderID eq $fakeTWCID) {
master_id_conflict();
}
# Other than picking a new fakeTWCID if ours conflicts with
# master, it doesn't seem that a real slave will make any
# sort of direct response when sent a master's linkready1.
}
elsif($msg =~ /\xc0\xfb\xe2(..)(.)\x00\x00\x00\x00\x00\x00\x00\x00.\xc0/s) {
# Handle linkready2 from master.
# See notes in send_master_linkready2() for details.
my $senderID = $1;
my $sign = $2;
# This message seems to always contain seven 00 bytes in its
# data area. If we ever get this message with non-00 data
# we'll print it as an unexpected message.
if($debugLevel >= 1) {
printf(time_now() . ": Master TWC %02x%02x wants to hook up. Sign: %s\n",
vec($senderID, 0, 8), vec($senderID, 1, 8),
hex_str($sign));
}
if($senderID eq $fakeTWCID) {
master_id_conflict();
}
# I seem to remember that a slave will respond with an
# immediate linkready when it sees master's linkready2. In
# fact, I think a real slave sends 5 copies of linkready
# about a second apart before returning to sending them once
# per 10 seconds. I don't bother emulating that since master
# will see one of our 10-second linkreadys eventually.
send_slave_linkready();
}
elsif($msg =~ /\xc0\xfb\xe0(..)(..)(.......).\xc0/s) {
# Handle heartbeat message from a master.
my $senderID = $1;
my $receiverID = $2;
my @heartbeatData = unpack('C*', $3);
if($receiverID ne $fakeTWCID) {
# This message was intended for another slave.
# Ignore it.
if($debugLevel >= 1) {
printf(time_now() . ": Master %02x%02x sent "
. "heartbeat message %s to receiver %02x%02x "
. "that isn't our fake slave.\n",
vec($senderID, 0, 8), vec($senderID, 1, 8),
hex_ary(@heartbeatData),
vec($receiverID, 0, 8), vec($receiverID, 1, 8));
}
next;
}
if($debugLevel >= 1) {
printf(time_now() . ": Master %02x%02x: %s Slave: %s\n",
vec($senderID, 0, 8), vec($senderID, 1, 8),
hex_ary(@heartbeatData), hex_ary(@slaveHeartbeatData));
}
# A real slave mimics master's status bytes [1]-[2]
# representing max charger power even if the master sends it
# a crazy value.
$slaveHeartbeatData[1] = $heartbeatData[1];
$slaveHeartbeatData[2] = $heartbeatData[2];
# Slaves always respond to master's heartbeat by sending
# theirs back.
send_slave_heartbeat($senderID);
}
elsif($msg =~ /\xc0\xfc\x1d\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00.\xc0/s) {
# Handle 4-hour idle message
#
# I haven't verified this, but TheNoOne reports receiving
# this message from a Master TWC three times in a row every
# 4 hours:
# c0 fc 1d 00 00 00 00 00 00 00 00 00 00 00 1d c0
# I suspect his logging was corrupted to strip the final fe
# byte, unless Tesla changed the protocol to 16 bytes just
# for this one message.
# I also suspect this message is only sent when the master
# doesn't see any other TWCs on the network, so I don't
# bother to have our fake master send these messages being
# as there's no point in playing a fake master with no
# slaves around.
if($debugLevel >= 1) {
print("Received 4-hour idle message from Master.\n");
}
}
else {
print(time_now() . ": ***UNKNOWN MESSAGE from master: " . hex_str($msg) . "\n");
}
}
}
}
}
close TWC;
################################################################################
# Begin subs
sub load_settings
{
if(open(my $fh, '<', $settingsFileName)) {
while(my $line = <$fh>) {
if($line =~ m/^\s*nonScheduledAmpsMax\s*=\s*([-0-9.]+)/m) {
$nonScheduledAmpsMax = $1;
if($debugLevel >= 10) {
print("load_settings: \$nonScheduledAmpsMax set to $nonScheduledAmpsMax\n");
}
}
if($line =~ m/^\s*scheduledAmpsMax\s*=\s*([-0-9.]+)/m) {
$scheduledAmpsMax = $1;
if($debugLevel >= 10) {
print("load_settings: \$scheduledAmpsMax set to $scheduledAmpsMax\n");
}
}
if($line =~ m/^\s*scheduledAmpsStartHour\s*=\s*([-0-9.]+)/m) {
$scheduledAmpsStartHour = $1;
if($debugLevel >= 10) {
print("load_settings: \$scheduledAmpsStartHour set to $scheduledAmpsStartHour\n");
}
}
if($line =~ m/^\s*scheduledAmpsEndHour\s*=\s*([-0-9.]+)/m) {
$scheduledAmpsEndHour = $1;
if($debugLevel >= 10) {
print("load_settings: \$scheduledAmpsEndHour set to $scheduledAmpsEndHour\n");
}
}
if($line =~ m/^\s*scheduledAmpsDaysBitmap\s*=\s*([-0-9.]+)/m) {
$scheduledAmpsDaysBitmap = $1;
if($debugLevel >= 10) {
print("load_settings: \$scheduledAmpsDaysBitmap set to $scheduledAmpsDaysBitmap\n");
}
}
if($line =~ m/^\s*hourResumeTrackGreenEnergy\s*=\s*([-0-9.]+)/m) {
$hourResumeTrackGreenEnergy = $1;
if($debugLevel >= 10) {
print("load_settings: \$hourResumeTrackGreenEnergy set to $hourResumeTrackGreenEnergy\n");
}
}
}
close $fh;
}
}
sub save_settings
{
if(open(my $fh, '>', $settingsFileName)) {
print $fh "nonScheduledAmpsMax=$nonScheduledAmpsMax\n"
. "scheduledAmpsMax=$scheduledAmpsMax\n"
. "scheduledAmpsStartHour=$scheduledAmpsStartHour\n"
. "scheduledAmpsEndHour=$scheduledAmpsEndHour\n"
. "scheduledAmpsDaysBitmap=$scheduledAmpsDaysBitmap\n"
. "hourResumeTrackGreenEnergy=$hourResumeTrackGreenEnergy\n";
close($fh);
}
}
sub new_slave
{
my ($newSlaveID, $maxAmps) = @_;
my $slaveTWC = $slaveTWCs{$newSlaveID};