seanyoung / cir

Linux Infrared Control
https://docs.rs/irp
22 stars 2 forks source link

Just a question #3

Closed kubycsolutions closed 4 months ago

kubycsolutions commented 2 years ago

I'm looking at handling the IR code for Mitsubishi minisplit HVAC systems, which is an 18-byte sequence (17 and checksum), repeated once to improve noise rejection. Basically, they chose to transmit the entire desired state of the machine every time rather than just sending a code for the single change the user has applied.

Looking at your tools, it appears that for this monster the higher-level tools won't help; I'm stuck with handling the I/O as raw LIRC timing sequences, parsing and generating those myself.

Correct? And if so, do you happen to know if anyone's handled this protocol? It shouldn't be difficult to code, since it's straight mapping of bitfields to/from meaningful values, but while that could be a good learning-Rust exercise I'd be glad to instead use/contribute to an existing effort.

seanyoung commented 2 years ago

I think this tool should be ideally suited for dealing with this kind of protocol. I don't know of anyone who has written an decoder/encoder for this device. The way to go about this is to write an IRP spec for it.

The difficulty is in describing the protocol in IRP. Would you mind describing the protocol in detail in words, and then we can work together towards implementing. So what signals does the remote always send, how are the following bits encoded, anything else trailing etc. Also the carrier frequency would be useful (if known).

kubycsolutions commented 2 years ago

Here's my text rewritten from multiple websites of folks who have reverse-engineered part (not all) of this, with values to illustrate usage.

MSZ-GF60VE and GF71VE (simple heads) Standard transmission: 2 identical frames of 18 bytes each With header, trailer, gap. OPEN LOOP.

Frame provides COMPLETE, ABSOLUTE desired new state. To support relative adjustments, you would need to track state locally. (They assume a single remote per head, or KumoCloud network adapter.) I have an IR sensor, in theory could receive and update my own state... but as soon as I make a change, the physical remote is going to be out of sync with what the unit is actually doing.)

HEADER: Byte 0: CONST 0x23 Byte 1: CONST 0xCB Byte 2: CONST 0x26 Byte 3: CONST 0x01 Byte 4: CONST 0x00

POWER: Byte 5: 0x20 ON, 0x00 OFF. (single meaningful bit)

MODE: Byte 6: 0x08 HEAT (three meaningful bits) 0x10 DEHUMIDIFY 0x18 COOL 0x20 "Auto"? (What's fan-only mode?)

TEMPERATURE: Byte 7: 0x0_ CELSIUS, low 4 bits, 16 added to specified value so range is 16c to 31c 23c is thus 0x07.

MODE AGAIN, but encoded differently Byte 8: 0x30 HEAT 0x32 DEHUMIDIFY 0x36 COOL 0x30 "Auto"? (What's fan-only?)

FAN SPEED, VERTICAL VANE Fvvvvfff Low three bits are fan speed Next four bit are vane position High bit F must be 1 when setting Fan Speed Auto, 0 when setting vanes only? Byte 9: 0x80 Fan auto, vane 0 0x34 Fan 4, Vane 6 0x43 Fan 3, vane AUTO (0b1000) 0x71 Fan 1, vane SWEEP (0x1111)

CLOCK SET: Encoded as 10-minute units since midnight (Note that remote's clock is more precise, but...) (Does KumoCloud sync better with NTP?) Byte 10 0x00 Midnight 0x01 12:10 AM 0x24 6 AM (36) 0x60 4 PM

STOP TIME: Same 10-minute count from midnight as clock BYTE 11: 0x00 No stop time set 0x84 Stop at 22:00

START TIME: Same 10-minute count from midnight Byte 12: 0x00 No start time set

SCHEDULE MODE (just enable/disable stop/start on these heads; fancier heads have a better scheduler and use a different set of packets to set it) Byte 13: Low three bits 0x05 Enable START only 0x03 Enable END only 0x07 Enable START and END 0x00 No programmed events

ADDITIONAL FIELDS, used in the fancier heads to control horizontal vanes, the ISEE sensor, PLASMA and CLEAN modes. Byte 14: CONST 0x00 Byte 15: CONST 0x00 Byte 16: CONST 0x00

CHECKSUM -- literally 8-bit-remainder sum of all previous Byte 17: SUM[Byte0-Byte16] & 0xFF

(Note: The KumoCloud hardware actually connects to the unit's own processor over RS-232. Folks have reverse-engineered that too, and it can run closed-loop... but I'm not ready yet to void my warranty by plugging into that. Anything that happens via the IR port is the manufacturer's problem.)

kubycsolutions commented 2 years ago

And yes, if you push the "temp up one degree" button on the remote, it retransmits everything, not just the button or a minimal change request.

I don't think this complex and stateful behavior is the kind of protocol your IRP was designed for. I may just not have gotten deep enough into it yet.

BTW, in the simpler heads start time and stop time are one-shot events, needing to be manually reasserted each day -- which is part of the reason I want to take over and run my own scheduler.

seanyoung commented 2 years ago

This looks like this protocol: https://github.com/r45635/HVAC-IR-Control/blob/master/Protocol/Mitsubishi_IR_Packet_Data_v1.1-FULL.pdf

In IRP that would be:

{38k}<450,-420|450,-1300>
(3400,-1750,0x23cb260100:40,power:8,mode:8,temp:8,hvac:8,fan:8,clock:8,end:8,start:8,timer:8,0:24,chk:8,440,-17100)2
{chk=0x23+0xcb+0x26+0x01+0x00+power+mode+temp+hvac+fan+clock+end+start+timer} 
[power:0..255,mode:0..255,temp:0..255,hvac:0..255,clock:0..255,start:0..255,end:0..255,timer:0..255]
seanyoung commented 2 years ago

To transmit this IR:

cir transmit irp '{38k}<450,-420|450,-1300>
(3400,-1750,0x23cb260100:40,power:8,mode:8,temp:8,hvac:8,fan:8,clock:8,end:8,start:8,timer:8,0:24,chk:8,440,-17100)2
{chk=0x23+0xcb+0x26+0x01+0x00+power+mode+temp+hvac+fan+clock+end+start+timer} 
[power:0..255,mode:0..255,temp:0..255,hvac:0..255,clock:0..255,start:0..255,end:0..255,timer:0..255]' -v -fpower=0x20 fmode...

(and the rest of the fields of course)

If you want to decode the IR, this can be done with:

cir decode --irp '{38k}<450,-420|450,-1300>
(3400,-1750,0x23cb260100:40,power:8,mode:8,temp:8,hvac:8,fan:8,clock:8,end:8,start:8,timer:8,0:24,chk:8,440,-17100)2
{chk=0x23+0xcb+0x26+0x01+0x00+power+mode+temp+hvac+fan+clock+end+start+timer} 
[power:0..255,mode:0..255,temp:0..255,hvac:0..255,clock:0..255,start:0..255,end:0..255,timer:0..255]' -vv

Note decoding is currently being written, so there may be bugs. Please send me the output so I can help find any issues.

kubycsolutions commented 2 years ago

Yes, that's the protocol I'm dealing with.

Ok, so irp can handle the packing and unpacking, and given that spec cir can (or will, after debugging) handle rejecting noise while waiting for the desired signal. That does simplify things a bit.

But to have a useful system I'm still going to need to front-end it with something that retains state so my own code doesn't force the user to re-enter things we don't want to change -- simplify it back to something closer to the button UI. And despite today's common practice, I'd rather not do that in a scripting language. Is there an API for cir/irp that I could invoke directly from rust rather than having to shell out to it? (Yes, I can go diving into your code to answer this and/or to repartition to establish one; just hoping to save a bit of time if this is documented.)

Also, while you've broken out the bytes, I don't think you've dealt with the fields within those bytes, so I still have to do some packing and unpacking in the front end.

seanyoung commented 2 years ago

Yes, that's the protocol I'm dealing with.

Ok, so irp can handle the packing and unpacking, and given that spec cir can (or will, after debugging) handle rejecting noise while waiting for the desired signal. That does simplify things a bit.

But to have a useful system I'm still going to need to front-end it with something that retains state so my own code doesn't force the user to re-enter things we don't want to change -- simplify it back to something closer to the button UI. And despite today's common practice, I'd rather not do that in a scripting language. Is there an API for cir/irp that I could invoke directly from rust rather than having to shell out to it? (Yes, I can go diving into your code to answer this and/or to repartition to establish one; just hoping to save a bit of time if this is documented.)

Absolutely. The IRP part is in its own rust crate: https://docs.rs/irp/ For dealing with lirc devices in linux, there is the cir crate: https://docs.rs/cir/ Let me know if you would like some example code.

Also, while you've broken out the bytes, I don't think you've dealt with the fields within those bytes, so I still have to do some packing and unpacking in the front end.

That is true, I did this so it would be simpler to calculate the checksum. The IRP should really have a slightly better definition which has all the functions broken out, and not just bytes. It's just a start.

kubycsolutions commented 2 years ago

Yes, sample code might be a Good Thing. (Might actually be a good thing to include in the crates, as documentation and additional test tools.)

I gave the IRP parse a spin.

Without -vv, there is no output at all from cir decode. And at first glance, the log doesn't actually seem to tell me what data values were extracted; I hope it's there and just buried in the other output.

In hope of getting the values displayed, I tried cir receive. Apparently it doesn't accept the --irp switch, which surprised me a bit -- I know irp is under development, but I'd have thought you'd want to implement that for ease of testing if nothing else.

kubycsolutions commented 2 years ago

I'm going to have to set this aside for a few weeks; I'll ping you when I can resume poking at it.

seanyoung commented 2 years ago

@kubycsolutions I've added some fixes to the decoder. Please try again with the latest version on main.

For this protocol, the tolerances can be increased.

cir decode --irp '{38k}<450,-420|450,-1300> \
(3400,-1750,0x23cb260100:40,power:8,mode:8,temp:8,hvac:8,fan:8,clock:8,end:8,start:8,timer:8,0:24,chk:8,440,-17100)2 \
{chk=0x23+0xcb+0x26+0x01+0x00+power+mode+temp+hvac+fan+clock+end+start+timer} \
[power:0..255,mode:0..255,temp:0..255,hvac:0..255,clock:0..255,start:0..255,end:0..255,timer:0..255]' -vv \
--absolute-tolerance 200 --relative-tolerance=10
seanyoung commented 2 years ago

Yes, sample code might be a Good Thing. (Might actually be a good thing to include in the crates, as documentation and additional test tools.)

There is an example at https://docs.rs/irp/0.2.2/irp/ although this does not include the lirc side of things.

I gave the IRP parse a spin.

Without -vv, there is no output at all from cir decode. And at first glance, the log doesn't actually seem to tell me what data values were extracted; I hope it's there and just buried in the other output.

Would you mind sending the output please, from the lasted main version. Right now the output is very verbose, which needs cleaning up.

In hope of getting the values displayed, I tried cir receive. Apparently it doesn't accept the --irp switch, which surprised me a bit -- I know irp is under development, but I'd have thought you'd want to implement that for ease of testing if nothing else.

cir receive for printing the raw lirc IR and decoded output as with the currently configured protocol decoder for the device, set with cir config. As you've discovered, this is not very clear. I'm not sure how this should be organized. Maybe cir test-config or something like that. Suggestions very welcome.

kubycsolutions commented 2 years ago

Back from (and recovered from) vacation...

I tried a git pull and cargo build, and I'm seeing a type error:

   Compiling evdev v0.12.0
error[E0308]: mismatched types
   --> /home/keshlamIR/.cargo/registry/src/github.com-1285ae84e5963aae/evdev-0.12.0/src/lib.rs:393:29
    |
393 |     let dur = Duration::new(tv.tv_sec.unsigned_abs(), tv.tv_usec as u32 * 1000);
    |                             ^^^^^^^^^^^^^^^^^^^^^^^^ expected `u64`, found `u32`
    |
help: you can convert a `u32` to a `u64`
    |
393 |     let dur = Duration::new(tv.tv_sec.unsigned_abs().into(), tv.tv_usec as u32 * 1000);
    |                                                     +++++++

Trying apt-get upgrade/cargo clean/cargo build didn't change the situation. Trying cargo update/cargo build... Nope, same issue. Went through a bunch of Rust updating... same complaint but a new line in the error message:

[...]
note: associated function defined here
help: you can convert a `u32` to a `u64`
[...]

I'm new to Rust, as I've said. Any insight into what's going on here and how to fix it would be GREATLY appreciated.

seanyoung commented 2 years ago

@kubycsolutions what cpu architecture and rust version are you using?

I'm guessing you're on a 32 bit arm cpu.

kubycsolutions commented 2 years ago

Raspberry Pi, so yes, I believe that's 32-bit ARM. I'm a bit surprised that Rust code wasn't written to anticipate that possibility; I guess the library authors too are still learning to use explicitly sized types...

I see the fix has been accepted. What's the process for upgrading to the new release?

seanyoung commented 2 years ago

Please pull the latest cir and it should build. I've pinned it to the version of evdev in git with the fix.

kubycsolutions commented 2 years ago

Ran the suggested test,

target/debug/cir decode --irp '{38k}<450,-420|450,-1300> (3400,-1750,\
0x23cb260100:40,power:8,mode:8,temp:8,hvac:8,fan:8,clock:8,end:8,start:8,timer:8,0:24,chk:8,440,-17100)2\
 {chk=0x23+0xcb+0x26+0x01+0x00+power+mode+temp+hvac+fan+clock+end+start+timer}\
 [power:0..255,mode:0..255,temp:0..255,hvac:0..255,clock:0..255,start:0..255,end:0..255,timer:0..255]'\
 -vv --absolute-tolerance 200 --relative-tolerance 10

(I've tried to undo the wordwrap produced by the github edit box; hope that worked...)

Good news is I get output to stderr (with the desired signal buried in a morass of background noise, apparently).

I don't quite grok what the output is trying to tell me, though, since pos: values seem to be jumping around. And the simple checksum appears to be failing -- it looks like you aren't truncating that sum down to a single byte, if I'm reading the assert FAIL correctly.

System was fighting me when trying to attach a tgz file, so you get a Windows zip of the stderr text. Sorry... m.err.zip

seanyoung commented 2 years ago

@kubycsolutions looks like a made a mistake in the irp, the first constant should be in a different byte order. If I feed cir the infrared I fished out of your log file, it decodes the IR:

$ cir decode --irp '{38k}<450,-420|450,-1300> (3400,-1750,
0x00126cb23:40,power:8,mode:8,temp:8,hvac:8,fan:8,clock:8,end:8,start:8,timer:8,0:24,chk:8,440,-17100)2
 {chk=0x23+0xcb+0x26+0x01+0x00+power+mode+temp+hvac+fan+clock+end+start+timer}
 [power:0..255,mode:0..255,temp:0..255,hvac:0..255,clock:0..255,start:0..255,end:0..255,timer:0..255]'  --absolute-tolerance 200 --relative-tolerance 10 --graphviz=nfa
decoded: $repeat=0, clock=100, end=0, hvac=54, mode=24, power=32, start=0, temp=23, timer=0

Notice that 0x23cb260100 has been replaced with 0x00126cb23.

kubycsolutions commented 2 years ago

Had to change ' to " before my shell was happy with it, but it's running without errors, and at first glance the data looks plausible.

Now I need to look at doing this thru API, sub-parsing and cross-checking, integrating the IR sender (does irp encode yet?) and putting my own statefull interface on all that so I can work with it from network and in Node-RED without having to explicitly re-specify everything when I want to change one parameter.

seanyoung commented 2 years ago

I've expanded the readme for the irp crate: https://github.com/seanyoung/cir/blob/main/irp/README.md

There now is an example for sending IR using a lirc device, see https://github.com/seanyoung/cir/tree/main/irp#sending-ir-using-cir-crate

Hope this helps.

seanyoung commented 4 months ago

Let me know if you need anything else