--- /dev/null
+#! /usr/bin/perl
+# * * * * * nice -n20 /root/bin/heat -s
+use strict;
+use warnings;
+$|=1;
+sub readfile($) {
+ my($fname)=@_;
+ local *F;
+ if (!open F,$fname) {
+ warn "$fname: $!";
+ return undef;
+ }
+ my $F=do { local $/; <F>; };
+ defined $F or die "read $fname: $!";
+ close F or die "read-close $fname: $!";
+ return $F;
+}
+sub writefile {
+ my($fname,$content)=@_;
+ local *F;
+ open F,">$fname" or die "$fname: $!";
+ print F $content or die "write $fname: $!";
+ close F or die "write-close $fname: $!";
+}
+sub spawn($) {
+ my($cmd)=@_;
+ $cmd.=" >&2";
+ system $cmd or return 1;
+ warn "$cmd: $!";
+ return 0;
+}
+my($trash,$min,$hour)=localtime;
+my $minutes=$hour*60+$min;
+if (($ARGV[0]||"")=~/^[@]0?(\d+):0?(\d+)$/) {
+ $minutes=$1*60+$2;
+ shift;
+}
+my $schedulefile="/root/heat.schedule";
+my $statefile="/root/heat.state";
+my $resetfile="/root/heat.reset";
+my $usbrelay="/root/bin/usbrelay";
+my $schedule=readfile $schedulefile;
+my $finishline;
+my %schedule;
+while ($schedule=~s/^(0?(\d+):0?(\d+)) ([01s])\n//) {
+ my $tm=$2*60+$3;
+ warn "$schedulefile set twice for: $1" if defined $schedule{$tm};
+ $schedule{$tm-24*60}="$1 $4\n";
+ $schedule{$tm }="$1 $4\n";
+ $schedule{$tm+24*60}="$1 $4\n";
+}
+warn "$schedulefile garbage: $schedule" if $schedule ne "";
+sub schedulenext($) {
+ my($now)=@_;
+ my($bestprev,$prev,$bestnext,$next);
+ for my $found (keys(%schedule)) {
+ if ($found<=$now) {
+ next if defined $bestprev&&$bestprev>$found;
+ $bestprev=$found;
+ $prev=$schedule{$found};
+ } else {
+ next if defined $bestnext&&$bestnext<$found;
+ $bestnext=$found;
+ $next=$schedule{$found};
+ }
+ }
+ die "No bestprev" if !defined $bestprev;
+ die "No bestnext" if !defined $bestnext;
+ return [$prev,$next];
+}
+$finishline=schedulenext $minutes;
+my $silent=shift if ($ARGV[0]||"") eq "-s";
+sub info($) {
+ my($s)=@_;
+ print $s if !$silent;
+}
+sub finish() {
+ info join("",@$finishline) if defined $finishline;
+ exit 0;
+}
+my $reset=readfile $resetfile if -e $resetfile;
+my($resetminutes,$resetstate);
+sub resetread() {
+ $reset=~/^0?(\d+):0?(\d+) ([01s]|reset)\n$/ or warn "Invalid $resetfile: $reset";
+ $resetminutes=$1*60+$2;
+ $resetstate=($3 eq "reset"?undef:$3);
+}
+if (defined $reset) {
+ resetread;
+ if ($resetminutes==$minutes) {
+ unlink $resetfile or die "$resetfile: $!";
+ $reset=$resetminutes=undef;
+ }
+}
+sub finishlinereset() {
+ $finishline=schedulenext $resetminutes;
+ splice @$finishline,1,0,$reset;
+}
+finishlinereset if defined $reset;
+my $state=readfile $statefile;
+chomp $state;
+info $state;
+my $newstate=shift;
+$newstate=$resetstate if !defined $newstate&&defined $resetstate&&!defined $resetminutes;
+sub printminutes($) {
+ my($m)=@_;
+ return sprintf "%02d:%02d",int($m/60),$m%60;
+}
+if ($silent) {
+ if (!defined $newstate&&!defined $reset) {
+ my $prev=schedulenext($minutes)->[0];
+ $newstate=$prev if $prev=~s{^@[ printminutes $minutes ] (.)\n$}{$1} or $prev=undef;
+ }
+ $newstate=$state if !defined $newstate;
+}
+if (!defined $newstate) {
+ info "\n";
+ finish;
+}
+die "state!={s|0|1}" if $newstate!~/^[s01]$/;
+sub setstate() {
+ info "->$newstate";
+ my $both={"s"=>[0,0],"0"=>[1,0],"1"=>[1,1]}->{$newstate};
+ $state eq $newstate or unlink $statefile or warn "$statefile: $!";
+ ( spawn "$usbrelay 1 ".$both->[0]
+ and spawn "$usbrelay 2 ".$both->[1])
+ or do { unlink $statefile; die "usbrelay error"; };
+ $state eq $newstate or writefile $statefile,"$newstate\n";
+ info "\n";
+}
+my $newreset=shift;
+die "Excessive args: ".join(" ",@ARGV) if @ARGV;
+if (!defined $newreset) {
+ setstate;
+ if (!$silent&&defined $reset) {
+ unlink $resetfile or die "$resetfile: $!";
+ $finishline=schedulenext $minutes;
+ }
+ finish;
+}
+$reset=$newreset;
+if ($reset=~/^\d+$/) {
+ $resetminutes=$reset+$minutes;
+ $reset=printminutes($resetminutes)." reset\n";
+ setstate;
+} else {
+ $reset.=" $newstate\n";
+ resetread;
+ info "\n";
+}
+writefile $resetfile,$reset;
+finishlinereset;
+finish;
--- /dev/null
+#!/usr/bin/env python3
+# -*- encoding: utf-8 -*-
+# https://slomkowski.eu/tutorials/eavesdropping-usb-and-writing-driver-in-python/
+
+import time
+import sys
+
+import usb.core
+import usb.util
+
+VENDOR_ID = 0x16c0
+DEVICE_ID = 0x05df
+
+MANUFACTURER_NAME = "www.dcttech.com"
+PRODUCT_NAME = "USBRelay2"
+
+
+def check_manufacturer_and_product(dev):
+ return dev.manufacturer == MANUFACTURER_NAME and dev.product == PRODUCT_NAME
+
+
+def find_device_handle():
+ return usb.core.find(idVendor=VENDOR_ID, idProduct=DEVICE_ID,
+ custom_match=check_manufacturer_and_product)
+
+
+dev_handle = find_device_handle()
+
+if dev_handle.is_kernel_driver_active(0):
+ try:
+ dev_handle.detach_kernel_driver(0)
+ print("kernel driver detached")
+ except usb.core.USBError as e:
+ sys.exit("Could not detach kernel driver: %s" % str(e))
+
+requestType = usb.util.build_request_type(usb.util.CTRL_OUT,
+ usb.util.CTRL_TYPE_CLASS,
+ usb.util.CTRL_RECIPIENT_INTERFACE)
+
+
+def set_relay(relay_number, enabled: bool):
+ b1 = 0xff if enabled else 0xfd
+ dev_handle.ctrl_transfer(requestType, 9, 0x0300, 0, (b1, relay_number, 0, 0, 0, 0, 0, 0))
+
+
+#while True:
+# set_relay(1, True)
+# time.sleep(1)
+# set_relay(1, False)
+# time.sleep(1)
+# set_relay(2, True)
+# time.sleep(1)
+# set_relay(2, False)
+# time.sleep(1)
+
+
+if len(sys.argv)!=3 or (sys.argv[1]!="1" and sys.argv[1]!="2") or (sys.argv[2]!="0" and sys.argv[2]!="1"):
+ sys.exit("usbrelay: {1|2} {0|1}")
+set_relay((1 if sys.argv[1]=="1" else 2),(True if sys.argv[2]=="1" else False))