├── LICENSE ├── README.md ├── README_jp.md ├── demo ├── M5StackATOM │ ├── GUI │ │ ├── communication.py │ │ ├── dataframe.py │ │ ├── fileframe.py │ │ ├── main.py │ │ ├── mainframe.py │ │ └── signalframe.py │ └── micropython │ │ └── main.py └── RP2040 │ ├── GUI │ ├── communication.py │ ├── dataframe.py │ ├── fileframe.py │ ├── main.py │ ├── mainframe.py │ └── signalframe.py │ └── micropython │ └── main.py ├── image ├── M5Stack.jpg ├── RX_Circuit.png ├── TX_Circuit.png ├── TxRx.jpg └── gui.png └── micropython ├── ESP32 └── FromV1_17 │ ├── UpyIrRx.py │ └── UpyIrTx.py └── RP2040 └── FromV1_17 ├── UpyIrRx.py └── UpyIrTx.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 meloncookie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RemotePy 2 | 3 | [ [English] ](https://github.com/meloncookie/RemotePy/blob/master/README.md) , 4 | [ [Japanese] ](https://github.com/meloncookie/RemotePy/blob/master/README_jp.md) 5 | 6 | This is an infrared remote control send / receive library by micropython. 7 | 8 | Fans, TVs, and even air conditioners with long data lengths operate stably 9 | regardless of the manufacturer. 10 | 11 | The microcomputer supports only the ESP32 chip and the RP2040 chip (Raspberry Pi Pico). 12 | It works on the original [micropython](https://micropython.org/). 13 | The supported version is v1.17 or higher. 14 | 15 | The send process uses a board-specific experimental micropython API. 16 | There is a risk that it will not work due to changes in specifications in the future. 17 | I have confirmed the operation up to v1.19.(2022/07) 18 | 19 | A sample program is also available for immediate use. 20 | You can try infrared remote control data acquisition / 21 | transmission with GUI application software for PC. 22 | 23 | --- 24 | 25 | ## Bundled files 26 | 27 | 1. Micropython firmware for ESP32 28 | - After micropython v1.17 29 | + micropython/ESP32/FromV1_17/UpyIrRx.py 30 | + micropython/ESP32/FromV1_17/UpyIrTx.py 31 | 32 | 2. Micropython firmware for RP2040 (Raspberry Pi Pico) 33 | - After micropython v1.17 34 | + micropython/RP2040/FromV1_17/UpyIrRx.py 35 | + micropython/RP2040/FromV1_17/UpyIrTx.py 36 | 37 | 3. Demo micropython main firmware 38 | + For M5Stack ATOM(Lite & MATRIX) : demo/M5StackATOM/micropython/main.py 39 | + For RP2040 (Raspberry Pi Pico) : demo/RP2040/micropython/main.py 40 | 41 | 4. Demonstration PC side python application software 42 | + For M5Stack ATOM(Lite & MATRIX) : demo/M5StackATOM/GUI 43 | + For RP2040 (Raspberry Pi Pico) : demo/RP2040/GUI 44 | --- 45 | 46 | ## Program procedure on the microcomputer board side 47 | * Download the firmware corresponding to the microcomputer board 48 | from the orginal [micropython](https://micropython.org/). 49 | * Write the micropython firmware to the microcontroller board. 50 | * Write UpyIrRx.py / UpyIrTx.py according to the microcomputer board. 51 | * Use these two libraries UpyIrRx.py / UpyIrTx.py to write your main program. 52 | Use UpyIrTx.py for infrared transmission and UpyIrRx.py for infrared reception. 53 | * Separately from the microcomputer board, prepare an external board 54 | for transmitting and receiving infrared remote control. 55 | 56 | --- 57 | 58 | ## External infrared remote control transmission / reception circuit 59 | 60 | ![TxRx](https://user-images.githubusercontent.com/70054769/170876136-5e2e392d-b7ca-4790-94bf-7cceee272171.jpg) 61 | 62 | ### *Transmission circuit* 63 | 64 | Infrared LEDs with an appropriate wavelength (wavelength around 950 nm) 65 | are controlled on and off with a microcomputer. 66 | The following circuits are ofte 67 | Connect the TxPin to the pins of the microcomputer. 68 | 69 | 70 | For the on section of the signal, a PWM wave with 38kHz and a duty ratio of 1/3 is used. 71 | The LED is off during the signal off section. 72 | Therefore, the sender has the following three parameters. 73 | This is specified in the constructor argument of the infrared transmission library UpyIrTx.py. 74 | (However, in the case of the RP2040 chip, You need to modify UpyIrTx.py directly.) 75 | 76 | 1. Signal modulation frequency (usually 38kHz) 77 | 2. Signal-on section duty ratio (usually 30%) 78 | 3. Microcomputer GPIO output level in signal off section "idle_level". 79 | (Low = 0 in the above circuit) 80 | 81 | ![TX_Circuit](https://user-images.githubusercontent.com/70054769/170875035-52dca65b-af3c-4995-b8ad-72cde2b86a7e.png) 82 | 83 | ### *Receive circuit* 84 | 85 | Use the infrared remote control light receiving module. 86 | Connect the output RxPin of the light receiving module to the pin of the microcomputer. 87 | Depending on the light receiving module, it may have an open collector output, 88 | in which case a pull-up resistor is installed. 89 | 90 | The receiver has the following one parameter. 91 | This is specified in the constructor argument of the infrared transmission library UpyIrRx.py. 92 | 93 | * Output level of infrared remote control light receiving module 94 | when there is no light receiving "idle_level". 95 | (High = 1 for general modules) 96 | 97 | ![RX_Circuit](https://user-images.githubusercontent.com/70054769/170875122-8a23d50c-663a-4415-909a-6cc82f63d144.png) 98 | 99 | ### *Convenient commercial module* 100 | 101 | If it is difficult to build a circuit with individual parts, 102 | it is a good idea to purchase a ready-made module. 103 | The [IR REMOTE UNIT](https://docs.m5stack.com/en/unit/ir), 104 | which can be connected with the Grove connector, is very convenient. 105 | In combination with M5Stack, infrared signals can reach distances of several meters. 106 | 107 | ![M5Stack](https://user-images.githubusercontent.com/70054769/170875733-371c58ea-0572-4239-bfab-3c58ea8ff3b9.jpg) 108 | 109 | --- 110 | 111 | ## How to use the infrared remote control reception library UpyIrRx.py (UpyIrRx class) 112 | 113 | The UpyIrRx class controls the reception process. 114 | The output voltage of the infrared remote control light receiving module 115 | is a digital signal as shown below. 116 | Record the time length of this digital signal in list format. 117 | This recorded data can be used in the infrared remote control transmission library below. 118 | 119 | 120 | The output level of the infrared remote control light receiving module is 121 | generally High (= 1) when no light is received (called idle_level). 122 | When this output switches to Low, remote control signal reception starts. 123 | If the output level does not change for a certain period of time 124 | (blank_time [msec]), it is regarded as the end of the remote control signal 125 | and the received data is confirmed. 126 | 127 | ``` 128 | __ ____ _________ ___________________ If idle_level = 1 129 | |_______| |_____| |_____| 130 | start <- blank time -> End of signal 131 | <- t0 ->< t1 >< t2 ><-- t3 -->< t4 > ... [usec] 132 | 133 | Waveform data list = [t0, t1, t2, t3, t4] 134 | ( The unit is usec, and the number of elements is odd. ) 135 | ``` 136 | 137 | 1. `__init__(pin, max_size=0, idle_level=1)` 138 | + Parameters 139 | - pin 140 | 141 | It is a pin object (machine.Pin object) of the pin that is the input of 142 | the received signal. 143 | It is a microcomputer pin connected to RxPin in the above circuit. 144 | 145 | - max_size: int 146 | 147 | Maximum length that can store the received signal 148 | (If the default value is 0, the data length is 1023.) 149 | 150 | - idle_level: int 151 | 152 | Output level of infrared remote control light receiving module 153 | when there is no light receiving 0/1 (High = 1 by default) 154 | 155 | 2. `record(wait_ms=0, blank_ms=0, stop_size=0) -> int` 156 | 157 | After waiting for wait_ms[msec], the remote control reception signal data is 158 | recorded in the internal variable. 159 | When this method is called, the previously recorded data will be discarded. 160 | The newly recorded data will be overwritten with internal variables. 161 | If it cannot be received normally, the recorded data will be invalid. 162 | Whether or not it was recorded normally is judged by the return value. 163 | 164 | + Parameters 165 | - wait_ms: int 166 | 167 | It is time [msec] to wait for the remote control reception signal. 168 | During this time, processing will be blocked. 169 | If the default value is 0, it will be 5000 [msec]. 170 | 171 | - blank_ms: int 172 | 173 | When the stationary section of the remote control received signal exceeds 174 | this time [msec], it is considered to be the end of the remote control signal. 175 | If the default value is 0, it will be 200 [msec]. 176 | 177 | - stop_size: int 178 | 179 | The part that exceeds this signal length is excluded from recording. 180 | If the default value is 0, no constraint is applied. 181 | However, if the max_size length of the constructor is exceeded, 182 | an overflow error will occur. 183 | 184 | + Return 185 | - UpyIrRx.ERROR_NONE (=0) : When recording is completed normally 186 | - UpyIrRx.ERROR_NO_DATA (=1) : If there is no significant signal in wait_ms time 187 | - UpyIrRx.ERROR_OVERFLOW (=2) : When the maximum received signal length specified 188 | in the constructor is exceeded 189 | - UpyIrRx.ERROR_START_POINT (=3) : If the output level is not idle_level at the 190 | time of calling the record () method 191 | - UpyIrRx.ERROR_END_POINT (=3) : When the output level is not idle_level 192 | at the end of the signal 193 | - UpyIrRx.ERROR_TIMEOUT (=4) : If the remote control signal does not meet the 194 | termination condition in wait_ms time 195 | 196 | 3. `get_mode() -> int` 197 | 198 | Acquires the remote control signal reception status. 199 | 200 | + Return 201 | - UpyIrRx.MODE_STAND_BY (=0) : record() has not been called yet 202 | - UpyIrRx.MODE_DONE_OK (=1) : A state in which normal recorded data is 203 | retained in the previous call to record(). 204 | - UpyIrRx.MODE_DONE_NG (=2) : The last call to record() caused some error 205 | and the recorded data is invalid. 206 | 207 | 4. `get_record_size() -> int` 208 | 209 | Acquires the signal length of the remote control reception signal recorded 210 | in the internal variable. 211 | It corresponds to the number of elements of the waveform data acquired by 212 | get_calibrate_list() below. 213 | 214 | + Return 215 | - The number of elements in the waveform data list. 216 | 217 | 5. `get_calibrate_list() -> list` 218 | 219 | Acquires the remote control received signal data recorded internally by 220 | the previous record() method. 221 | This list is used when sending remote control signals. 222 | If there is no normal recorded data, an empty list will be acquired. 223 | 224 | + Return 225 | - A one-dimensional list of int type with an odd number of elements. 226 | Empty list if there is no normal recorded data. 227 | Even if this method is called, the internal recorded data is not 228 | destroyed and is retained. 229 | 230 | Waveform data that has been calibrated to exclude the influence of the delay 231 | characteristics of the infrared remote control light receiving module is acquired. 232 | 233 | A sister version of the method is get_record_list(). 234 | This is not recommended as it will capture uncalibrated raw data. 235 | Sending uncalibrated remote control signals is often misidentified. 236 | 237 | --- 238 | 239 | ## How to use the infrared remote control transmission library UpyIrTx.py (UpyIrTx class) 240 | 241 | The UpyIrTx class is responsible for the send process. 242 | The infrared remote control signal is transmitted based on the remote control 243 | reception signal data acquired by the above UpyIrRx object. 244 | 245 | * If idle_level = 0 246 | ``` 247 | LED OFF time _______ _____ _____ LED OFF time 248 | _____________| |____| |_________| |_________ 249 | ``` 250 | * If idle_level = 1 251 | ``` 252 | _____________ ____ _________ _________ 253 | |_______| |_____| |_____| 254 | 255 | <- t0 ->< t1 >< t2 ><-- t3 -->< t4 > ... [usec] 256 | ON time ON time ON time 257 | 258 | signal_tuple = (t0, t1, t2, t3, t4) 259 | ``` 260 | 261 | The ON section is the PWM waveform. You can specify the PWM frequency and duty ratio. 262 | 263 | 1. `__init__(ch, pin, freq=38000, duty=30, idle_level=0))` 264 | + Parameters 265 | - ch: int 266 | 267 | The channel number is 0-7. 268 | On the ESP32, this is the RMT peripheral channel number. 269 | On the RP2040, this is the state machine number of the PIO peripheral. 270 | Specify an unused number. If you haven't used it elsewhere, 0 is fine. 271 | 272 | - pin 273 | 274 | A pin object (machine.Pin object) for the pin that outputs the transmitted signal. 275 | It is a microcomputer pin connected to TxPin in the above circuit. 276 | 277 | - freq: int *1 278 | 279 | PWM frequency [Hz] in the ON section. 280 | The default value is 38000 [Hz]. 281 | 282 | - duty: int *1 283 | 284 | Duty ratio of PWM waveform in ON section 1-100 [%]. 285 | The default value is 30 [%]. 286 | 287 | - idle_level: int *1 288 | 289 | Logic level corresponding to infrared LED OFF 0/1 (Low with default 0)。 290 | In the general transmission circuit example above, it will be Low (=0). 291 | 292 | *1: In the case of RP2040 chip, this argument is invalid. 293 | If you want to change it, change the following constants in the source code UpyIrTx.py. 294 | 295 | ```python 296 | def pio_wave(): 297 | T = const(26) # Period: 1/38kHz*1M [us] (= OF_TIM + ON_TIM) 298 | OF_TIM = const(18) # Duty(30%) off time [us] 299 | OF_POR = const(0) # Idle level 300 | ON_TIM = const(8) # Duty(30%) on time [us] 301 | ON_POR = const(1) # not Idle level 302 | ``` 303 | 304 | 2. `send(signal_tuple: tuple) -> bool` 305 | 306 | The transmission signal is output according to the time list of the argument. 307 | It will be blocked until the transmission is completed. 308 | 309 | + Parameters 310 | - signal_tuple: tuple or list 311 | 312 | Specify a tuple or list of time information. 313 | Use the remote control received signal data acquired by the above UpyIrRx object. 314 | The number of elements is limited to odd numbers. 315 | 316 | + Return 317 | - Returns the success or failure of the transmission as a bool type. 318 | 319 | --- 320 | 321 | ## Program example 322 | 323 | This is a program example when [M5Stack ATOM](https://docs.m5stack.com/en/core/atom_matrix) 324 | and [IR REMOTE UNIT](https://docs.m5stack.com/en/unit/ir) are connected 325 | with a Grove connector. 326 | Although the [M5Stack ATOM](https://docs.m5stack.com/en/core/atom_matrix) 327 | main unit contains an infrared transmission circuit, 328 | it is not practical because it can fly only a very short distance. 329 | 330 | ```python 331 | from machine import Pin 332 | from UpyIrTx import UpyIrTx 333 | from UpyIrRx import UpyIrRx 334 | 335 | rx_pin = Pin(32, Pin.IN) # Pin No.32 336 | rx = UpyIrRx(rx_pin) 337 | 338 | tx_pin = Pin(26, Pin.OUT) # Pin No.26 339 | tx = UpyIrTx(0, tx_pin) # 0ch 340 | ... 341 | # If the remote control is transmitted to the receiving circuit 342 | # within 3000 msec, the remote control signal is acquired. 343 | rx.record(3000) 344 | if rx.get_mode() == UpyIrRx.MODE_DONE_OK: 345 | signal_list = rx.get_calibrate_list() 346 | # ex) [430, 1290, 430, 430, 430, 860, ...] 347 | else: 348 | signal_list = [] 349 | ... 350 | if signal_list: 351 | tx.send(signal_list) 352 | ... 353 | ``` 354 | 355 | --- 356 | 357 | ## Demo application 358 | 359 | 360 | In the attached demo application, along with the application software on the PC side, 361 | You can easily collect signals from the infrared remote controller and test the transmission. 362 | 363 | * Uses the same command channel as the REPL environment 364 | * Intuitive operability on the GUI screen 365 | * Record infrared remote control signal and create json file 366 | * Can be edited by calling a new or existing json file 367 | * Can be tested by sending a recording signal 368 | 369 | > --- caution --- 370 | > 371 | > The main.py written to the microcomputer is automatically executed after the power is turned on. 372 | > Inside main.py, the keystroke input () is in an infinite loop. 373 | > In this state, writing the program to the microcomputer will be blocked. 374 | > If you want to return to the REPL environment, 375 | > use the terminal software for serial communication and enter a line feed after the q key. 376 | > (Or Ctrl+c) 377 | 378 | ### *Microcomputer side preparation For M5Stack ATOM(Lite & Matrix)* 379 | 380 | The demo program runs on a system with 381 | [M5Stack ATOM](https://docs.m5stack.com/en/core/atom_matrix) 382 | and [IR REMOTE UNIT](https://docs.m5stack.com/en/unit/ir) connected via a Grove connector. 383 | Write the three files "main.py", "UpyIrRx.py", and "UpyIrTx.py" to the microcomputer. 384 | As in the REPL environment, connect the PC side and M5Stack with a USB cable. 385 | 386 | If you want to use another ESP32 module, modify the source code as shown in main.py below. 387 | 388 | ### *Microcomputer side preparation For RP2040(Raspberry Pi Pico)* 389 | 390 | Write the three files "main.py", "UpyIrRx.py", and "UpyIrTx.py" to the microcomputer. 391 | An external infrared transmitter / receiver circuit can be connected to any GPIO pin. 392 | In this example, 393 | Connect the output of the infrared remote control light receiving module to GPIO Pin.18, 394 | The infrared remote control transmission signal is connected to GPIO Pin.19. 395 | 396 | If you want to use other pins, rewrite the following pin layout in the source code. 397 | 398 | **Modifications of main.py** 399 | ```python 400 | _GROVE_PIN = {'ATOM': (32, 26), 401 | 'CORE2': (33, 32), 402 | 'BASIC': (22, 21), 403 | 'GRAY': (22, 21), 404 | 'FIRE': (22, 21), 405 | 'GO': (22, 21), 406 | 'Stick': (33, 32), 407 | 'Else': (18, 19)} # Rewrite (RxPin Number, TxPin Number) 408 | _DEVICE = 'Else' # Rewrite 'Else' 409 | _TX_IDLE_LEVEL = const(0) # Sender idle_level(Invalid when using RP2040) 410 | _TX_FREQ = const(38000) # Sender modulation frequency(Invalid when using RP2040) 411 | _TX_DUTY = const(30) # Sender duty ratio(Invalid when using RP2040) 412 | _RX_IDLE_LEVEL = const(1) # Receiver idle_level 413 | ``` 414 | 415 | ### *PC side preparation* 416 | 417 | The application on the PC side is written in python. 418 | It has been confirmed to work on windows, Ubuntu, and Raspbian OS. 419 | It uses the GUI framework Tkinter. 420 | 421 | ![gui](https://user-images.githubusercontent.com/70054769/172902379-3fce461f-63b1-4cf4-a9a1-cd01fe665a29.png) 422 | 423 | 1. Install python 3.8 or above. 424 | 425 | 2. Install serial communication library [pySerial](https://pythonhosted.org/pyserial/). 426 | 427 | `$ pip install pyserial` 428 | 429 | 3. The python program consists of 6 files. 430 | 431 | 4. If you want to use a microcomputer board other than the sample example, modify the following part of communication.py. 432 | Specify the vendor ID (VID) and product ID (PID) of the USB device. 433 | These IDs can be easily found from your PC. 434 | 435 | ```python 436 | class Communication(): 437 | _DEFAULT_VID = 1027 # A unique USB VID is assigned to each microcomputer board. 438 | _DEFAULT_PID = 24577 # Also USB PID 439 | ``` 440 | 441 | | Type | VID | PID | 442 | | :--- | ---: | ---: | 443 | | M5Stack ATOM | 1027 | 24577 | 444 | | M5Stack Core2 | 4292 | 60000 | 445 | | Raspberry Pi Pico | 11914 | 5 | 446 | | | | | 447 | 448 | 4. Start the program `$ python main.py` 449 | 450 | ### *How to use application software on the PC side* 451 | 452 | 1. Enter the name of the existing save file or the new save file in the **file** field. 453 | * You can also select it like a file explorer from the **Select** button. 454 | 2. Press the **Open** button to start editing. 455 | * The **Save** button is disabled at this point. It will be effective after some editing. 456 | Press to save and then close the file. 457 | * On the other hand, the **Close** button is always valid. 458 | Press to close the file without saving. 459 | 3. **Key list** is the key name of the remote control signal you named. 460 | * There is always only one `__sysytem__` key. Use it to comment on files. 461 | * One key consists of a remote control signal and a comment. 462 | 4. To add a new remote control signal key name to **Key_list**, fill in the **Edit key** field, then 463 | Press the **Append** button. 464 | 5. To delete the key name of the remote control signal of **Key_list**, 465 | select the **Key_list** you want to delete, and then press the **Delete** button. 466 | 6. To rename the key name of the remote control signal of **Key_list**, 467 | select the **Key_list** you want to rename, fill in the **Edit key** field, 468 | and press the **Rename** button. 469 | 7. Select the key name of the remote control signal in **Key_list** and press the **Record** button 470 | to record the remote control signal of that key name. 471 | Within Wait [sec] after pressing the button, let's send the remote control signal 472 | to the infrared remote control receiver module. 473 | * The waveform data is displayed in the **Signal** field. 474 | * You can freely record your comments in the **Comment** field. 475 | It is a good idea to describe the meaning of the signal (for example, "volume up"). 476 | * If the field is changed, the background will be red. 477 | * Press the **Commit** button to confirm. Press the **Cancel** button to return to the original data. 478 | * If you select another key name for **Key_list** without pressing the **Commit** button, 479 | it will be considered as **Cancel**. 480 | * For this reason, it's a good idea to press **Commit** button as soon as you edit (= the background turns red). 481 | If you press the **Commit** button, the background of the edited part will return to green. 482 | 8. Pressing the **Send** button will now send the remote control signal in the **Signal** column. 483 | 9. When you have finished editing steps 3-8, click the **Save** or **Close** button to close the file. 484 | * Press the **Save** button to save and exit your edits. 485 | * Click the **Close** button to discard all previous edits and exit. 486 | 487 | ### *Generated json file* 488 | 489 | This is a json format text file. 490 | In dictionary format, the key is the key name of the remote control signal. 491 | Value is in dictionary format. 492 | 493 | The value dictionary format consists of two keys, "signal" and "comment". 494 | A sample is illustrated below. 495 | 496 | ```json 497 | {"vol_up": {"signal": [430, 1290, 430, ...], "comment": "volume up"}, 498 | "ch1": {"signal": [430, 1290, 860, ...], "comment": "CH 1"}, 499 | ... 500 | } 501 | ``` 502 | 503 | --- 504 | 505 | ## Afterword 506 | 507 | Due to the limit of processing speed, 508 | sending and receiving remote control signals using micropython was difficult. 509 | 510 | Especially for the generation of the transmission signal, 511 | the jitter is too severe for the waveform generation using the sleep process, and it is not practical. 512 | This library is limited to ESP32 and RP2040 chips, but is board-specific. 513 | I overcame it by using the function. 514 | Considering the convenience of micropython, even if you have such a hard time, 515 | it is worth making it into a library. 516 | 517 | You can also use Home IoT in combination with WIFI. 518 | An automated system for air conditioning by timer processing is also practical. 519 | Make use of this library and have a fun electronic work life! 520 | -------------------------------------------------------------------------------- /README_jp.md: -------------------------------------------------------------------------------- 1 | # RemotePy 2 | 3 | [ [English] ](https://github.com/meloncookie/RemotePy/blob/master/README.md) , 4 | [ [Japanese] ](https://github.com/meloncookie/RemotePy/blob/master/README_jp.md) 5 | 6 | micropython 版の、赤外線リモコン送受信ライブラリです。 7 | 8 | Arduino 版には、便利なライブラリ 9 | [IRremote](https://github.com/Arduino-IRremote/Arduino-IRremote) 10 | があります。しかし C/C++ 言語の壁に挫折されている方も多いでしょう。 11 | より手軽な micropython で、必要最小限の機能を実現したライブラリです。 12 | 13 | 扇風機、テレビ、果てはデータ長の長いエアコンまで、メーカに依存せず、 14 | 安定して動作します。 15 | 16 | マイコンは ESP32チップ と RP2040チップ(Raspberry Pi Pico)のみ対応しています。 17 | 本家 [micropython](https://micropython.org/) 上で動作します。 18 | 対応バージョンは v1.17以上です。 19 | 20 | 送信側の処理は、ボード固有の実験的 micropython API を使用しています。 21 | 将来仕様の変更に伴い、動作しなくなるリスクがあります。 22 | (2022/7 現在 v1.19 までは動作確認しております。) 23 | 24 | すぐに使えるように、サンプルプログラムも用意しています。PC側の 25 | GUI アプリケーションソフトで、赤外線リモコンデータ収集/送信を、 26 | 試すことができます。 27 | 28 | --- 29 | 30 | ## 同梱ファイル 31 | 32 | 1. ESP32用の micropython ファームウェア 33 | - micropython v1.17以降 34 | + micropython/ESP32/FromV1_17/UpyIrRx.py 35 | + micropython/ESP32/FromV1_17/UpyIrTx.py 36 | 37 | 2. RP2040 (Raspberry Pi Pico) 用の micropython ファームウェア 38 | - micropython v1.17以降 39 | + micropython/RP2040/FromV1_17/UpyIrRx.py 40 | + micropython/RP2040/FromV1_17/UpyIrTx.py 41 | 42 | 3. デモ用の micropython メインファームウェア 43 | + M5Stack ATOM(Lite & MATRIX) 用 : demo/M5StackATOM/micropython/main.py 44 | + RP2040 (Raspberry Pi Pico) 用 : demo/RP2040/micropython/main.py 45 | 46 | 4. デモ用の PC 側 python アプリケーションソフト 47 | + M5Stack ATOM(Lite & MATRIX) 用 : demo/M5StackATOM/GUI 48 | + RP2040 (Raspberry Pi Pico) 用 : demo/RP2040/GUI 49 | 50 | --- 51 | 52 | ## マイコンボード側のプログラム手順 53 | 54 | * 本家 [micropython](https://micropython.org/) から、 55 | マイコンボードに応じたファームウェアをダウンロードします。 56 | * マイコンボードに micropython ファームウェアを書込みます。 57 | * マイコンボードに応じた UpyIrRx.py / UpyIrTx.py を、 58 | マイコンボードに書込みます。 59 | * これら2つのライブラリ UpyIrRx.py / UpyIrTx.py を使って、 60 | メインプログラムを記述します。赤外線送信は UpyIrTx.py を、 61 | 赤外線受信は UpyIrRx.py を用います。 62 | * マイコンボードとは別に、赤外線送受信する外付け基板を用意します。 63 | 64 | --- 65 | 66 | ## 外付け赤外線リモコン送受信回路 67 | 68 | ![TxRx](https://user-images.githubusercontent.com/70054769/170876136-5e2e392d-b7ca-4790-94bf-7cceee272171.jpg) 69 | 70 | ### *送信回路* 71 | 72 | 適切な波長(波長950nm前後) の赤外線LEDを、マイコンで 73 | 電流オンオフ制御します。下記の回路が、良く使われます。 74 | TxPin をマイコンのピンに接続します。 75 | 76 | 信号のオン区間は 38kHz, Duty比 1/3 のPWM波が使われます。 77 | 信号のオフ区間は LED 消灯しています。よって、送信側には以下の3つの 78 | パラメータがあります。これは、赤外線送信ライブラリ UpyIrTx.py の 79 | コンストラクタ引数で明示します。(但し、RP2040 チップの場合は、 80 | UpyIrTx.py を直接修正する必要があります。) 81 | 82 | 1. 信号変調周波数 (通常 38kHz) 83 | 2. 信号オン区間Duty比 (通常 30%) 84 | 3. 信号オフ区間の、マイコンGPIO出力レベル idle_level (上記回路では Low = 0) 85 | 86 | ![TX_Circuit](https://user-images.githubusercontent.com/70054769/170875035-52dca65b-af3c-4995-b8ad-72cde2b86a7e.png) 87 | 88 | ### *受信回路* 89 | 90 | 赤外線リモコン受光モジュールを使います。受光モジュールの出力 RxPin を 91 | マイコンのピンに接続します。受光モジュールによっては、オープンコレクタ 92 | 出力の場合があり、その場合はプルアップ抵抗を取り付けます。 93 | 94 | 受信側には以下の1つのパラメータがあります。これは、赤外線送信ライブラリ 95 | UpyIrRx.py のコンストラクタ引数で明示します。 96 | 97 | * 受光無い場合の、赤外線リモコン受光モジュールの出力レベル idle_level 98 | (一般的なモジュールでは High = 1) 99 | 100 | ![RX_Circuit](https://user-images.githubusercontent.com/70054769/170875122-8a23d50c-663a-4415-909a-6cc82f63d144.png) 101 | 102 | ### *便利なモジュール* 103 | 104 | 個別部品で回路を組むのが大変な場合、出来合いのモジュールを購入すると 105 | 良いでしょう。Grove コネクタで接続できる 106 | [IR REMOTE UNIT](https://docs.m5stack.com/en/unit/ir) 107 | は、大変便利です。M5Stack と組み合わせて、数m の距離まで 108 | 赤外線信号が届きます。 109 | 110 | ![M5Stack](https://user-images.githubusercontent.com/70054769/170875733-371c58ea-0572-4239-bfab-3c58ea8ff3b9.jpg) 111 | 112 | --- 113 | 114 | ## 赤外線リモコン受信ライブラリ UpyIrRx.py (UpyIrRxクラス) の使い方 115 | 116 | UpyIrRx クラスが、受信処理を司ります。赤外線リモコン受光モジュールの出力電圧は、以下の様に 117 | デジタル信号です。このデジタル信号の時間長を、リスト形式で記録します。この記録データは 118 | 、下記赤外線リモコン送信ライブラリで使えます。 119 | 120 | 赤外線リモコン受光モジュールの出力レベルは、無受光時に High (=1) が一般的です(idle_levelと呼称)。 121 | この出力が Low に切り替わった時点で、リモコン信号受信開始とします。 122 | 123 | 出力レベルが、一定時間以上(blank_time [msec]) 変化しなかったら、リモコン信号の終端と 124 | みなして、受信データを確定します。 125 | 126 | ``` 127 | __ ____ _________ ___________________ idle_level = 1 の場合の図 128 | |_______| |_____| |_____| 129 | start <- blank time -> ここでリモコン信号終端と見なす 130 | <- t0 ->< t1 >< t2 ><-- t3 -->< t4 > ... [usec] 131 | 132 | 波形データリスト = [t0, t1, t2, t3, t4] (単位は usec、要素数は奇数) 133 | ``` 134 | 135 | 1. `__init__(pin, max_size=0, idle_level=1)` 136 | + パラメータ 137 | - pin 138 | 139 | 受信信号の入力となるピンのピンオブジェクト (machine.Pin オブジェクト) です。 140 | 上記回路図の RxPin に接続されるマイコンピンに相当します。 141 | 142 | - max_size: int 143 | 144 | 受信信号を保存出来る最大長 (デフォルト値 0 だと 1023データ長) 145 | 146 | - idle_level: int 147 | 148 | 受光無い場合の、赤外線リモコン受光モジュールの出力レベル 0/1 (デフォルト1 で High) 149 | 150 | 2. `record(wait_ms=0, blank_ms=0, stop_size=0) -> int` 151 | 152 | wait_ms[msec] 時間だけブロッキングして、リモコン受信信号データを内部変数に記録します。 153 | 本メソッドを呼び出すと、前の記録済データは破棄され、新しく記録されたデータで内部変数に 154 | 上書きされます。正常に受信できなかった場合は、無効な記録データになります。 155 | 正常に記録されたかどうかは、戻り値で判断します。 156 | 157 | + パラメータ 158 | - wait_ms: int 159 | 160 | リモコン受信信号を待ち受ける時間 [msec] です。この時間だけ、処理がブロッキングされます。 161 | デフォルト値 0 の場合、5000[msec] になります。 162 | 163 | - blank_ms: int 164 | 165 | リモコン受信信号の静止区間が本時間 [msec] を超えた時点で、リモコン信号終端とみなします。 166 | デフォルト値 0 の場合、200[msec] になります。 167 | 168 | - stop_size: int 169 | 170 | リモコン受信信号長の制約個数で、この信号長を超過した部分は、記録除外されます。 171 | デフォルト値 0 の場合、制約を加えません。但し、コンストラクタの max_size 長 172 | を超えた場合は、オーバーフローエラーになります。 173 | 174 | + 戻り値 175 | - UpyIrRx.ERROR_NONE (=0) : 正常に記録完了の場合 176 | - UpyIrRx.ERROR_NO_DATA (=1) : wait_sec [msec] 内に有意な信号無い場合 177 | - UpyIrRx.ERROR_OVERFLOW (=2) : コンストラクタで指定した、最大受信信号長を超過した場合 178 | - UpyIrRx.ERROR_START_POINT (=3) : record() メソッドを呼び出し時点で、出力レベルが idle_level で無い場合 179 | - UpyIrRx.ERROR_END_POINT (=3) : 信号終端時の、出力レベルが idle_level で無い場合 180 | - UpyIrRx.ERROR_TIMEOUT (=4) : wait_sec [msec] 内に、リモコン信号が終端条件を満たしていない場合 181 | 182 | 3. `get_mode() -> int` 183 | 184 | リモコン信号受信状態を取得します。 185 | 186 | + 戻り値 187 | - UpyIrRx.MODE_STAND_BY (=0) : 未だ record() 呼び出されていない状態 188 | - UpyIrRx.MODE_DONE_OK (=1) : 前回 record() 呼び出しで、有意な記録済データを保有している状態 189 | - UpyIrRx.MODE_DONE_NG (=2) : 前回 record() 呼び出しで、何らかの異常が発生し、記録済データ 190 | が無効な状態 191 | 192 | 4. `get_record_size() -> int` 193 | 194 | 内部変数に記録されたリモコン受信信号の信号長を取得します。下記の get_calibrate_list() で取得される 195 | 波形データの要素数に相当します。 196 | 197 | + 戻り値 198 | - 波形データリストの要素数。 199 | 200 | 5. `get_calibrate_list() -> list` 201 | 202 | 前回の record() メソッドにより内部に記録されたリモコン受信信号データを、波形データリストとして 203 | 取得します。このリストは、リモコン信号送信時に利用されます。正常な記録データが無い場合は、 204 | 空リストが取得されます。 205 | 206 | + 戻り値 207 | - int型で、要素数が奇数の1次元リスト。 正常な記録データが無い場合は、空リスト。 208 | 本メソッドを呼び出しても、内部の記録データは破棄されず、そのまま保持されます。 209 | 210 | 赤外線リモコン受光モジュールの遅延特性の影響を、除外する校正 211 | 処理を行った波形データが取得されます。 212 | 213 | 姉妹版メソッドに、get_record_list() があります。こちらは、非校正の生データが 214 | 取得されるため、推奨しません。校正されていないリモコン信号 215 | を元に、送信すると誤認識する場合が多々発生します。 216 | 217 | --- 218 | 219 | ## 赤外線リモコン送信ライブラリ UpyIrTx.py (UpyIrTxクラス) の使い方 220 | 221 | UpyIrTx クラスが、送信処理を司ります。上記 UpyIrRx オブジェクトで取得される、 222 | リモコン受信信号データを元に、赤外線リモコン信号を送信します。 223 | 224 | 225 | * idle_level = 0 の場合 226 | ``` 227 | LED OFF区間 _______ _____ _____ LED OFF区間 228 | _____________| |____| |_________| |_________ 229 | ``` 230 | * idle_level = 1 の場合 231 | ``` 232 | _____________ ____ _________ _________ 233 | |_______| |_____| |_____| 234 | 235 | <- t0 ->< t1 >< t2 ><-- t3 -->< t4 > ... [usec] 236 | ON 区間 ON 区間 ON 区間 237 | 238 | signal_tuple = (t0, t1, t2, t3, t4) 239 | ``` 240 | 241 | ON 区間は、PWM波形です。PWM周波数と、Duty比を指定出来ます。 242 | 243 | 1. `__init__(ch, pin, freq=38000, duty=30, idle_level=0))` 244 | + パラメータ 245 | - ch: int 246 | 247 | 0-7 のチャンネル番号です。 248 | ESP32 では、RMTペリフェラルのチャンネル番号です。 249 | RP2040 では、PIOペリフェラルのステートマシン番号です。 250 | 未使用の番号を指定します。他に使っていなければ 0 で良いでしょう。 251 | 252 | - pin 253 | 254 | 送信信号の出力となるピンのピンオブジェクト (machine.Pin オブジェクト) です。 255 | 上記回路図の TxPin に接続されるマイコンピンに相当します。 256 | 257 | - freq: int ※1 258 | 259 | ON区間のPWM周波数 [Hz]。デフォルト値は 38000 [Hz]。 260 | 261 | - duty: int ※1 262 | 263 | ON区間のPWM波形のDuty比 1-100 [%]。デフォルト値は 30 [%] 264 | 265 | - idle_level: int ※1 266 | 267 | 赤外線LEDがOFFに相当する、論理レベル 0/1 (デフォルト0 で Low)。 268 | 上記の一般的送信回路例では、Low になります。 269 | 270 | ※1: RP2040 チップの場合、本引数は無効となる。変更したい場合は、ソースコード UpyIrTx.py 271 | の以下の定数を変更します。 272 | 273 | ```python 274 | def pio_wave(): 275 | T = const(26) # Period: 1/38kHz*1M [us] (= OF_TIM + ON_TIM) 276 | OF_TIM = const(18) # Duty(30%) off time [us] 277 | OF_POR = const(0) # Idle level 278 | ON_TIM = const(8) # Duty(30%) on time [us] 279 | ON_POR = const(1) # not Idle level 280 | ``` 281 | 282 | 2. `send(signal_tuple: tuple) -> bool` 283 | 284 | 引数の時間リストに従って、送信信号を出力します。送信完了までブロッキングされます。 285 | 286 | + パラメータ 287 | - signal_tuple: tuple or list 288 | 289 | 時間情報のタプル又はリストを指定。上記 UpyIrRx オブジェクトで取得される 290 | リモコン受信信号データを利用します。要素数は奇数に限ります。 291 | 292 | + 戻り値 293 | - 送信の成否を bool型で返します。 294 | 295 | --- 296 | 297 | ## プログラム事例 298 | 299 | ESP32 内蔵の [M5Stack ATOM](https://docs.m5stack.com/en/core/atom_matrix) と、 300 | [IR REMOTE UNIT](https://docs.m5stack.com/en/unit/ir) を Grove コネクタで接続した 301 | 場合のプログラム事例です。 302 | [M5Stack ATOM](https://docs.m5stack.com/en/core/atom_matrix) 303 | 本体には、赤外線送信回路が入っていますが、極短い距離しか飛ばないため 304 | 実用的ではありません。 305 | 306 | ```python 307 | from machine import Pin 308 | from UpyIrTx import UpyIrTx 309 | from UpyIrRx import UpyIrRx 310 | 311 | rx_pin = Pin(32, Pin.IN) # Pin No.32 312 | rx = UpyIrRx(rx_pin) 313 | 314 | tx_pin = Pin(26, Pin.OUT) # Pin No.26 315 | tx = UpyIrTx(0, tx_pin) # 0ch 316 | ... 317 | # 3000msec 以内に、受信回路に向けてリモコン送信すると、リモコン信号取得される。 318 | rx.record(3000) 319 | if rx.get_mode() == UpyIrRx.MODE_DONE_OK: 320 | signal_list = rx.get_calibrate_list() 321 | # ex) [430, 1290, 430, 430, 430, 860, ...] 322 | else: 323 | signal_list = [] 324 | ... 325 | # 送信回路から、リモコン信号を送信する。送信完了までブロッキング。 326 | if signal_list: 327 | tx.send(signal_list) 328 | ... 329 | ``` 330 | 331 | --- 332 | 333 | ## デモアプリケーション 334 | 335 | 付属のデモアプリケーションでは、PC側アプリケーションソフトと併せて、 336 | 簡単に赤外線リモコンの信号収集と、送信テストが出来ます。 337 | 338 | * REPL環境と同じコマンド通信路を使用 339 | * GUI画面で直感的な操作性 340 | * 赤外線リモコン信号を記録し json ファイル化 341 | * 新規または、既存 jsonファイルを呼び出して編集可能 342 | * その場で記録信号を送信しテスト可能 343 | 344 | > --- 注意 --- 345 | > 346 | > マイコンに書込んだ main.py は、電源投入後に自動的に実行されます。 347 | > main.py の内部では、キー入力 input() が無限ループされています。 348 | > このままでは、マイコンへのプログラム書込みがブロックされてしまいます。 349 | > REPL環境に戻りたい場合は、ターミナルソフトでシリアル通信して、 350 | > q キーの後改行を入力してください。(又は Ctrl+C キーも可です。) 351 | 352 | ### *マイコン側準備 M5Stack ATOM(Lite & Matrix) 版* 353 | 354 | デモプログラムでは、[M5Stack ATOM](https://docs.m5stack.com/en/core/atom_matrix) と、 355 | [IR REMOTE UNIT](https://docs.m5stack.com/en/unit/ir) を Grove コネクタで接続した 356 | システムに準拠しています。マイコンに、"main.py", "UpyIrRx.py", "UpyIrTx.py" の 357 | 3つのファイルを書込みます。REPL環境下と同じく、PC側と M5Stack 間を、USBケーブルで 358 | 接続します。 359 | 360 | 他のESP32モジュールを使用する場合は、下記の main.py の通りに、ソースコードを修正します。 361 | 362 | ### *マイコン側準備 RP2040(Raspberry Pi Pico) 版* 363 | 364 | マイコンに、"main.py", "UpyIrRx.py", "UpyIrTx.py" の 365 | 3つのファイルを書込みます。 366 | 任意のGPIOピンに、外付け赤外線送受信回路を接続出来ます。本例では、 367 | 赤外線リモコン受光モジュールの出力をGPIO Pin.18 に接続し、 368 | 赤外線リモコン送信信号をGPIO Pin.19 に接続しています。 369 | 370 | 他のピンを利用する場合は、ソースコードの以下のピン配置を書き換えます。 371 | 372 | **main.py の修正箇所** 373 | ```python 374 | _GROVE_PIN = {'ATOM': (32, 26), 375 | 'CORE2': (33, 32), 376 | 'BASIC': (22, 21), 377 | 'GRAY': (22, 21), 378 | 'FIRE': (22, 21), 379 | 'GO': (22, 21), 380 | 'Stick': (33, 32), 381 | 'Else': (18, 19)} # (RxPin番号, TxPin番号) に書換え 382 | _DEVICE = 'Else' # 'Else' に書換え 383 | _TX_IDLE_LEVEL = const(0) # 送信側の idle_level(RP2040使用時は無効) 384 | _TX_FREQ = const(38000) # 送信側 ON区間の変調周波数(RP2040使用時は無効) 385 | _TX_DUTY = const(30) # 送信側 ON区間のDuty比(RP2040使用時は無効) 386 | _RX_IDLE_LEVEL = const(1) # 受信側の idle_level 387 | ``` 388 | 389 | ### *PC側準備* 390 | 391 | PC側のアプリケーションは python で書かれています。windows, Ubuntu, Raspbian OS で動作確認済です。 392 | GUIフレームワーク Tkinter を用いています。 393 | 394 | ![gui](https://user-images.githubusercontent.com/70054769/172902379-3fce461f-63b1-4cf4-a9a1-cd01fe665a29.png) 395 | 396 | 1. python 3.8 以上をインストールします。 397 | 398 | 2. シリアル通信ライブラリ [pySerial](https://pythonhosted.org/pyserial/) をインストールします。 399 | 400 | `$ pip install pyserial` 401 | 402 | 3. pythonプログラムは6つのファイルから構成されています。 403 | 404 | 4. サンプル事例以外のマイコンボードを使う場合は、communication.py の以下の箇所を修正します。 405 | マイコン側 USBデバイスの、ベンダID(VID) とプロダクトID(PID) を指定します。 406 | これらの ID は PC から容易に調べることが出来ます。 407 | 408 | ```python 409 | class Communication(): 410 | _DEFAULT_VID = 1027 # マイコンボード毎に固有のUSB VIDが割り振られている。 411 | _DEFAULT_PID = 24577 # 同じくUSB PID 412 | ``` 413 | 414 | | Type | VID | PID | 415 | | :--- | ---: | ---: | 416 | | M5Stack ATOM | 1027 | 24577 | 417 | | M5Stack Core2 | 4292 | 60000 | 418 | | Raspberry Pi Pico | 11914 | 5 | 419 | | | | | 420 | 421 | 5. プログラムの起動は `$ python main.py` です。 422 | 423 | ### *PC側アプリケーションソフトの使い方* 424 | 425 | 1. 既存のセーブファイル名又は、新規セーブファイル名を **File** 欄に記入します。 426 | * **Select** ボタンから、ファイルエクスプローラ風に選択することも出来ます。 427 | 2. **Open** ボタンを押して編集を開始します。 428 | * この時点では **Save** ボタンは無効です。何らかの編集をしたら有効になります。 429 | セーブしてからファイルをクローズする場合に押します。 430 | * 一方 **Close** ボタンは常に有効です。セーブをせずにファイルをクローズする 431 | 場合に押します。 432 | 3. **Key list** は、自分で命名したリモコン信号のキー名です。(例えば、テレビの音量大 = "vol_up") 433 | * 必ず `__sysytem__` キーが唯一存在しています。ファイルのコメント用に使うと良いでしょう。 434 | * 一つのキーは、リモコン信号と、コメントから構成されています。 435 | 4. **Key_list** に新しいリモコン信号のキー名を追加するには、**Edit key** 欄に記入してから、 436 | **Append** ボタンを押します。 437 | 5. **Key_list** のリモコン信号のキー名を削除するには、消したい **Key_list** を選択してから、 438 | **Delete** ボタンを押します。 439 | 6. **Key_list** のリモコン信号のキー名を変名するには、変名したい **Key_list** を選択して、 440 | **Edit key** 欄に記入して、**Rename** ボタンを押します。 441 | 7. **Key_list** にあるリモコン信号のキー名を選択して、**Record** ボタンを押すと、そのキー名のリモコン信号 442 | を記録します。ボタン押した後 Wait [sec] 以内に、赤外線リモコン受信モジュールに向けて、 443 | リモコン信号を送信します。 444 | * **Signal** 欄に波形データが表示されます。 445 | * **Comment** 欄に自由にコメントを記録できます。信号の意味(例えば、"volume up") を記述する 446 | と良いでしょう。 447 | * 変更された場合は背景が赤になります。 448 | * **Commit** ボタンを押して確定します。**Cancel** すると元に戻ります。 449 | * **Commit** ボタンを押さずに、**key_list** の別のキー名を選択すると **Cancel** とみなします。 450 | * このため、編集したら(= 背景が赤になる) すぐに **Commit** ボタンを押すと良いでしょう。**Commit** すると、 451 | 編集箇所の背景は緑に戻ります。 452 | 8. **Send** ボタンを押すと、現在 **Signal** 欄のリモコン信号が送信されます。 453 | 9. 手順3-8の編集が全て終わったら、**Save** 又は **Close** ボタンでファイルをクローズします。 454 | * 編集内容をセーブして終了するには、**Save** ボタンです。 455 | * これまでの編集内容を全部破棄して終了するには、**Close** ボタンです。 456 | 457 | ### *生成される json ファイル* 458 | 459 | json形式のテキストファイルです。辞書形式で、キーはリモコン信号のキー名です。バリューは 460 | 辞書形式です。 461 | バリューの辞書形式は、"signal" と "comment" の2つキーから構成されています。 462 | サンプルを下記に例示します。 463 | 464 | ```json 465 | {"vol_up": {"signal": [430, 1290, 430, ...], "comment": "volume up"}, 466 | "ch1": {"signal": [430, 1290, 860, ...], "comment": "CH 1"}, 467 | ... 468 | } 469 | ``` 470 | 471 | --- 472 | 473 | ## あとがき 474 | 475 | 処理速度の限界で、micropython を使ったリモコン信号送受信は、 476 | 意外と大変でした。特に、送信信号の生成は、sleep 処理を使った 477 | 波形生成では、ジッタが酷過ぎて実用に耐えません。 478 | 479 | 本ライブラリは、ESP32, RP2040 のチップ限定ですが、ボード固有 480 | 機能を使うことで克服しました。micropython の利便性を考えると、 481 | ここまで苦労してでも、ライブラリ化した価値はあります。 482 | 483 | WIFIと組み合わせて Home IoT にしても良いでしょう。タイマー処理 484 | による、空調の自動化システムも実用的です。本ライブラリを活用して、 485 | 楽しい電子工作ライフを! 486 | -------------------------------------------------------------------------------- /demo/M5StackATOM/GUI/communication.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import serial 4 | from serial.tools import list_ports 5 | import time 6 | import json 7 | 8 | class Communication(): 9 | """Class to communicate with the device 10 | 11 | The communication path of this class is USB-UART. 12 | Separate the communication path from the GUI. 13 | """ 14 | # M5Stack ATOM (LITE) 15 | _DEFAULT_VID = 1027 16 | _DEFAULT_PID = 24577 17 | 18 | # M5Stack Core2 19 | # _DEFAULT_VID = 4292 20 | # _DEFAULT_PID = 60000 21 | 22 | # RaspberryPi pico 23 | # _DEFAULT_VID = 11914 24 | # _DEFAULT_PID = 5 25 | 26 | def __init__(self, device_name: str='', vid: int=0, pid: int=0): 27 | """Initialize 28 | 29 | Parameters 30 | ---------- 31 | device_name: str 32 | USB device name (ex. 'COM12' in Windows, '/dev/ttyUSB0' in Linux) 33 | For the empty string, the following two parameters are valid. 34 | vid: int 35 | Vendor ID of USB device. If 0, the default value _DEFAULT_VID is applied. 36 | If the device_name parameter is not an empty string, it does not apply. 37 | pid: int 38 | Product ID of USB device If 0, the default value _DEFAULT_PID is applied. 39 | If the device_name parameter is not an empty string, it does not apply. 40 | """ 41 | if device_name: 42 | self._device = device_name 43 | else: 44 | self._device = None 45 | self.connect('', vid, pid) 46 | 47 | def connect(self, device_name: str='', vid: int= 0, pid: int = 0) -> bool: 48 | """Connection 49 | 50 | Disable the currently connected device and connect to the new device. 51 | The device is active only at the moment of sending and receiving data. 52 | Therefore, it is not necessary to connect after disconnecting. 53 | 54 | Parameters 55 | ---------- 56 | device_name: str 57 | USB device name (ex. 'COM12' in Windows, '/dev/ttyUSB0' in Linux) 58 | For the empty string, the following two parameters are valid. 59 | vid: int 60 | Vendor ID of USB device. If 0, the default value _DEFAULT_VID is applied. 61 | If the device_name parameter is not an empty string, it does not apply. 62 | pid: int 63 | Product ID of USB device If 0, the default value _DEFAULT_PID is applied. 64 | If the device_name parameter is not an empty string, it does not apply. 65 | """ 66 | if device_name: 67 | self._device = device_name 68 | return(True) 69 | devlis = list_ports.comports() 70 | if vid <= 0 or pid <= 0: 71 | _vid = Communication._DEFAULT_VID 72 | _pid = Communication._DEFAULT_PID 73 | else: 74 | _vid = vid 75 | _pid = pid 76 | for i in devlis: 77 | if i.vid == _vid and i.pid == _pid: 78 | self._device = i.device 79 | return(True) 80 | self._device = None 81 | return(False) 82 | 83 | def disconnect(self) -> None: 84 | """Disconnect 85 | 86 | Disable the communication path. 87 | """ 88 | self._device = None 89 | 90 | def is_connect(self) -> bool: 91 | """Whether it is connected 92 | 93 | Returns 94 | ---------- 95 | bool 96 | """ 97 | if self._device: 98 | return(True) 99 | else: 100 | return(False) 101 | 102 | def send(self, msg: str, timeout: float=2) -> bool: 103 | """Turn on the infrared signal 104 | 105 | Parameters 106 | ---------- 107 | msg: str 108 | Data sent to the device. The format is 109 | "w[400, 1200, 400, ...]\r\n" 110 | 111 | Returns 112 | ---------- 113 | bool 114 | Whether communication was successful. 115 | """ 116 | if not self.is_connect(): 117 | return(False) 118 | try: 119 | with serial.Serial(self._device, baudrate=115200, timeout=timeout, 120 | write_timeout=2) as ser: 121 | ser.reset_input_buffer() 122 | ser.reset_output_buffer() 123 | ser.write(msg.encode()) 124 | start_time = time.time() 125 | # Detect echo back 126 | ack = b'' 127 | while not ack: 128 | ack = ser.readline() 129 | if time.time() - start_time > timeout: 130 | raise Exception() 131 | # Detect ack 132 | ack = b'' 133 | while not ack: 134 | ack = ser.readline() 135 | if time.time() - start_time > timeout: 136 | raise Exception() 137 | # Wait a moment before disconnecting. 138 | time.sleep(0.5) 139 | if b'OK' in ack: 140 | return(True) 141 | else: 142 | return(False) 143 | except: 144 | self.disconnect() 145 | return(False) 146 | 147 | def record(self, msg: str, timeout: float=4) -> tuple: 148 | """Get infrared received signal 149 | 150 | Parameters 151 | ---------- 152 | msg: str 153 | Data sent to the device. The format is 154 | "r[4000, 200, 1023]\r\n" 155 | The first factor is the reception timeout time [msec] 156 | The second element is the static duration [msec] 157 | that recognizes the end of reception. 158 | The third factor is the upper limit of the received signal length. 159 | 160 | Returns 161 | ---------- 162 | tuple (item1, item2) 163 | item1: bool 164 | Communication error 165 | item2: list 166 | Integer list meaning received signal. 167 | If it fails, an empty list is returned. 168 | """ 169 | if not self.is_connect(): 170 | return((False, [])) 171 | try: 172 | with serial.Serial(self._device, baudrate=115200, timeout=timeout, 173 | write_timeout=2) as ser: 174 | ser.reset_input_buffer() 175 | ser.reset_output_buffer() 176 | ser.write(msg.encode()) 177 | start_time = time.time() 178 | # Detect echo back 179 | ack = b'' 180 | while not ack: 181 | ack = ser.readline() 182 | if time.time() - start_time > timeout: 183 | raise Exception() 184 | # Detect ack 185 | ack = b'' 186 | while not ack: 187 | ack = ser.readline() 188 | if time.time() - start_time > timeout: 189 | raise Exception() 190 | # Wait a moment before disconnecting. 191 | time.sleep(0.5) 192 | return([True, json.loads(ack)]) 193 | except: 194 | self.disconnect() 195 | return((False, [])) 196 | -------------------------------------------------------------------------------- /demo/M5StackATOM/GUI/dataframe.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import tkinter as Tk 4 | from tkinter import messagebox 5 | import json 6 | import os 7 | 8 | from numpy import isin 9 | 10 | class ScrolledListbox(Tk.Listbox): 11 | """ Listbox widget with vertical scroll bar""" 12 | def __init__(self, master, **key): 13 | """Initialize extended widget "ScrolledListbox" 14 | 15 | inputs 16 | ---------- 17 | master : Parent frame 18 | key : option dictionary 19 | """ 20 | self.yscroll = Tk.Scrollbar(master, orient=Tk.VERTICAL) 21 | self.yscroll.pack(side=Tk.RIGHT, fill=Tk.Y, expand=1) 22 | key['yscrollcommand']=self.yscroll.set 23 | super().__init__(master, **key) 24 | self.pack(side=Tk.LEFT, fill=Tk.BOTH, expand=1) 25 | self.yscroll.config(command=self.yview) 26 | 27 | class DataFrame(Tk.Frame): 28 | """Class of control data 29 | 30 | When remote controler data is registered, data management and 31 | display control are performed. 32 | 33 | Data format(dictionary) 34 | ---------- 35 | {'action key1': {'signal': [100, 20, ...], 'comment': 'xxx'}, 36 | 'action key2': {'signal': [360, 90, ...], 'comment': 'yyy'}, 37 | .... 38 | '__system__': {'signal': [], 'comment': 'meta comment'} 39 | } 40 | 41 | Remote control data consists of time sequence data and 42 | annotations with action (CH1, REC, etc...) as a key. 43 | 44 | The action key is displayed in a list. The current choice 45 | of the list is automatically identified. 46 | 47 | A special action key '__system__' means a comment for this entire data. 48 | This key always exists and cannot be erased. 49 | """ 50 | # Widget size 51 | HEIGHT_KEYLIST = 10 52 | WIDTH_KEYLIST = 18 53 | # Special key name 54 | SYSTEM_KEY = '__system__' 55 | # State 56 | STATE_CLOSED = 0 57 | STATE_OPENED = 1 58 | STATE_EDITED = 2 59 | 60 | def __init__(self, master, **key): 61 | """Initialize sub frame for data-control 62 | 63 | 1. Register the remote control data read from the file. 64 | 2. Data management, information update, and information 65 | display are performed. 66 | 67 | Parameters 68 | ---------- 69 | master : Parent frame 70 | key : option dictionary 71 | 72 | GUI Widget 73 | ---------- ---------- 74 | ________ 75 | | Open | self.open_button (Button) 76 | ________ 77 | | Save | self.save_button (Button) 78 | ________ 79 | | Close | self.close_button (Button) 80 | 81 | key 82 | ----------- 83 | | | self.key_list (ScrolledListbox) 84 | | | 85 | | | 86 | | | 87 | ----------- 88 | 89 | Select key 90 | ___________ 91 | | | self.select_key (Entry) 92 | ___________ self.select_var (StringVar) 93 | | Delete | self.delete_button (Button) 94 | 95 | Edit key 96 | ___________ 97 | | | self.edit_key (Entry) 98 | ___________ self.edit_var (StringVar) 99 | | Append | self.append_button (Button) 100 | ___________ 101 | | Rename | self.rename_button (Button) 102 | 103 | """ 104 | super().__init__(master, **key) 105 | self.master = master 106 | # inner frame 107 | self.inner_frame0 = Tk.Frame(self) 108 | self.inner_frame1 = Tk.Frame(self) 109 | self.inner_frame2 = Tk.Frame(self) 110 | # variable 111 | self.select_var = Tk.StringVar(value='') 112 | self.edit_var = Tk.StringVar(value='') 113 | 114 | # widget in frame0 115 | self.open_button = Tk.Button(self.inner_frame0, 116 | text='Open', 117 | command=self._open) 118 | self.open_button['state'] = Tk.NORMAL 119 | self.save_button = Tk.Button(self.inner_frame0, 120 | text='Save', 121 | command=self._save) 122 | self.save_button['state'] = Tk.DISABLED 123 | self.close_button = Tk.Button(self.inner_frame0, 124 | text='Close', 125 | command=self._close) 126 | self.close_button['state'] = Tk.DISABLED 127 | self.open_button.pack(fill=Tk.X, padx=10, pady=5) 128 | self.save_button.pack(fill=Tk.X, padx=10, pady=5) 129 | self.close_button.pack(fill=Tk.X, padx=10, pady=5) 130 | 131 | # widget in frame1 132 | Tk.Label(self.inner_frame1, text='Key list').pack(anchor=Tk.W) 133 | self.key_list = ScrolledListbox(self.inner_frame1, 134 | selectmode=Tk.SINGLE, 135 | height=DataFrame.HEIGHT_KEYLIST, 136 | width=DataFrame.WIDTH_KEYLIST) 137 | self.key_list.pack(fill=Tk.X, padx=10, pady=5) 138 | 139 | # widget in frame2 140 | Tk.Label(self.inner_frame2, text='Select key').pack(anchor=Tk.W) 141 | self.select_key = Tk.Label(self.inner_frame2, 142 | textvariable=self.select_var, 143 | relief='ridge') 144 | self.select_key.pack(fill=Tk.X, padx=10, pady=5) 145 | self.delete_button = Tk.Button(self.inner_frame2, 146 | text='Delete', 147 | command=self._delete) 148 | self.delete_button['state'] = Tk.DISABLED 149 | self.delete_button.pack(fill=Tk.X, padx=10, pady=5) 150 | 151 | Tk.Label(self.inner_frame2, text='Edit key').pack(anchor=Tk.W) 152 | self.edit_key = Tk.Entry(self.inner_frame2, 153 | textvariable=self.edit_var) 154 | self.edit_key['state'] = Tk.DISABLED 155 | self.edit_key.pack(fill=Tk.X, padx=10, pady=5) 156 | self.append_button = Tk.Button(self.inner_frame2, 157 | text='Append', 158 | command=self._append) 159 | self.append_button['state'] = Tk.DISABLED 160 | self.append_button.pack(fill=Tk.X, padx=10, pady=5) 161 | self.rename_button = Tk.Button(self.inner_frame2, 162 | text='Rename', 163 | command=self._rename) 164 | self.rename_button['state'] = Tk.DISABLED 165 | self.rename_button.pack(fill=Tk.X, padx=10, pady=5) 166 | 167 | # Register callback when list is selected 168 | self.key_list.bind('<>', self._key_list_select) 169 | # pack inner frame 170 | self.inner_frame0.pack(fill=Tk.X, padx=10, pady=5) 171 | self.inner_frame1.pack(fill=Tk.X, padx=10, pady=5) 172 | self.inner_frame2.pack(fill=Tk.X, padx=10, pady=5) 173 | # data 174 | self.save_data = {DataFrame.SYSTEM_KEY: {'signal':[], 'comment': ''}} 175 | self.save_keys = [DataFrame.SYSTEM_KEY] 176 | self.select_index = 0 177 | self.state = DataFrame.STATE_CLOSED 178 | 179 | def _key_list_select(self, event): 180 | """Callback when list is selected""" 181 | widget = event.widget 182 | tpl_select = widget.curselection() 183 | if(tpl_select): 184 | self.select_index = tpl_select[0] 185 | self._view_information() 186 | 187 | def _open(self): 188 | """Operation when the open button is pressed""" 189 | try: 190 | filename = self.master.get_filename() 191 | if os.path.isfile(filename): 192 | with open(filename) as fp: 193 | self.save_data = json.load(fp) 194 | # Type check 195 | if not isinstance(self.save_data, dict): 196 | raise Exception() 197 | for key in self.save_data: 198 | if not isinstance(self.save_data[key], dict): 199 | raise Exception() 200 | else: 201 | self.save_data = {DataFrame.SYSTEM_KEY: {'signal':[], 'comment': ''}} 202 | self.save_keys = list(self.save_data.keys()) 203 | if DataFrame.SYSTEM_KEY not in self.save_keys: 204 | self.save_data[DataFrame.SYSTEM_KEY] = {'signal': [], 'comment': ''} 205 | else: 206 | self.save_keys.remove(DataFrame.SYSTEM_KEY) 207 | self.save_keys.sort() 208 | self.save_keys.insert(0, DataFrame.SYSTEM_KEY) 209 | except: 210 | messagebox.showerror('Error', "Can't open file or Data format is illegal.") 211 | return 212 | self.key_list.delete(0, Tk.END) 213 | self.key_list.insert(Tk.END, *self.save_keys) 214 | self.select_index = 0 215 | self.key_list.selection_set(self.select_index) 216 | self._view_information() 217 | 218 | self.open_button['state'] = Tk.DISABLED 219 | self.save_button['state'] = Tk.DISABLED 220 | self.close_button['state'] = Tk.NORMAL 221 | self.master.file_lock() 222 | self.state = DataFrame.STATE_OPENED 223 | self.delete_button['state'] = Tk.NORMAL 224 | self.edit_key['state'] = Tk.NORMAL 225 | self.append_button['state'] = Tk.NORMAL 226 | self.rename_button['state'] = Tk.NORMAL 227 | 228 | def _save(self): 229 | """Operation when the save button is pressed""" 230 | try: 231 | with open(self.master.get_filename(), mode='w') as fp: 232 | json.dump(self.save_data, fp) 233 | except: 234 | messagebox.showerror('Error', "Can't save safely.") 235 | return 236 | self.save_button['state'] = Tk.DISABLED 237 | self.state = DataFrame.STATE_OPENED 238 | 239 | def _close(self): 240 | """Operation when the close button is pressed""" 241 | if self.state == DataFrame.STATE_EDITED: 242 | ack = messagebox.askyesno('Data has been updated', 'Do you want to close without saving?') 243 | if ack is False: 244 | return 245 | self.open_button['state'] = Tk.NORMAL 246 | self.save_button['state'] = Tk.DISABLED 247 | self.close_button['state'] = Tk.DISABLED 248 | self.select_var.set('') 249 | self.key_list.delete(0, Tk.END) 250 | self.delete_button['state'] = Tk.DISABLED 251 | self.edit_key['state'] = Tk.NORMAL 252 | self.edit_var.set('') 253 | self.edit_key['state'] = Tk.DISABLED 254 | self.append_button['state'] = Tk.DISABLED 255 | self.rename_button['state'] = Tk.DISABLED 256 | self.save_data = {} 257 | self.select_index = 0 258 | self.state = DataFrame.STATE_CLOSED 259 | self.master.signal_disable() 260 | self.master.file_unlock() 261 | 262 | def _delete(self): 263 | """Operation when the delete button is pressed""" 264 | delete_key = self.save_keys[self.select_index] 265 | if delete_key == DataFrame.SYSTEM_KEY: 266 | return 267 | del self.save_data[delete_key] 268 | del self.save_keys[self.select_index] 269 | self.key_list.delete(self.select_index) 270 | if len(self.save_keys) <= self.select_index: 271 | self.select_index -= 1 272 | self.key_list.select_clear(0, Tk.END) 273 | self.key_list.select_set(self.select_index) 274 | self.state = DataFrame.STATE_EDITED 275 | self.save_button['state'] = Tk.NORMAL 276 | self._view_information() 277 | 278 | def _append(self): 279 | """Operation when the append button is pressed""" 280 | append_key = self.edit_var.get() 281 | if append_key == '' or append_key in self.save_keys: 282 | return 283 | self.save_data[append_key] = {'signal': [], 'comment': ''} 284 | self.save_keys.append(append_key) 285 | self.key_list.insert(Tk.END, append_key) 286 | self.select_index = len(self.save_keys) - 1 287 | self.key_list.select_clear(0, Tk.END) 288 | self.key_list.select_set(self.select_index) 289 | self.state = DataFrame.STATE_EDITED 290 | self.save_button['state'] = Tk.NORMAL 291 | self._view_information() 292 | 293 | def _rename(self): 294 | """Operation when the rename button is pressed""" 295 | now_key = self.save_keys[self.select_index] 296 | rename_key = self.edit_var.get() 297 | if rename_key == '' or rename_key in self.save_keys\ 298 | or now_key == DataFrame.SYSTEM_KEY: 299 | return 300 | self.save_data[rename_key] = self.save_data[now_key] 301 | del self.save_data[now_key] 302 | self.save_keys[self.select_index] = rename_key 303 | self.key_list.delete(self.select_index) 304 | self.key_list.insert(self.select_index, rename_key) 305 | self.key_list.select_clear(0, Tk.END) 306 | self.key_list.select_set(self.select_index) 307 | self.state = DataFrame.STATE_EDITED 308 | self.save_button['state'] = Tk.NORMAL 309 | self._view_information() 310 | 311 | def _view_information(self): 312 | """Display signal / comment data by signalframe.py""" 313 | self.select_var.set(self.save_keys[self.select_index]) 314 | selected = self.save_data.get(self.save_keys[self.select_index]) 315 | selected_signal = selected.get('signal', []) 316 | selected_comment = selected.get('comment', '') 317 | self.master.signal_enable(selected_signal, selected_comment) 318 | 319 | def update(self, signal_list: list, comment_text: str): 320 | """Data update process from signalframe.py""" 321 | self.save_data[self.save_keys[self.select_index]] =\ 322 | {'signal': signal_list.copy(), 'comment': comment_text} 323 | self.state = DataFrame.STATE_EDITED 324 | self.save_button['state'] = Tk.NORMAL 325 | -------------------------------------------------------------------------------- /demo/M5StackATOM/GUI/fileframe.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import tkinter as Tk 4 | from tkinter import filedialog 5 | import os 6 | 7 | class FileFrame(Tk.Frame): 8 | """Class of file select""" 9 | # Length of character string in the file_entry field 10 | WIDTH_FILENAME = 60 11 | 12 | def __init__(self, master, **key): 13 | """Initialize sub frame for file select 14 | 15 | ********** 16 | GUI 17 | ********** 18 | --------------- -------- 19 | File: | | | Select | 20 | --------------- -------- 21 | 22 | ********** 23 | Widget 24 | ********** 25 | self.file_entry (file_entry) 26 | self.filename_var (StringVar) 27 | self.file_select (Button) 28 | """ 29 | super().__init__(master, **key) 30 | # inner frame 31 | self.inner_frame0 = Tk.Frame(self) 32 | # variable 33 | self.filename_var = Tk.StringVar() 34 | # widget in frame0 35 | Tk.Label(self.inner_frame0, text='File').pack(side=Tk.LEFT) 36 | self.file_entry = Tk.Entry(self.inner_frame0, 37 | textvariable=self.filename_var, 38 | width=FileFrame.WIDTH_FILENAME) 39 | self.file_entry.pack(side=Tk.LEFT, fill=Tk.X, padx=5) 40 | self.file_select = Tk.Button(self.inner_frame0, text='Select', 41 | command=self._file_select) 42 | self.file_select.pack(side=Tk.LEFT, padx=10) 43 | # pack inner frame 44 | self.inner_frame0.pack(fill=Tk.X, padx=10, pady=5) 45 | # others 46 | self.current_dir = os.path.curdir 47 | 48 | def _file_select(self): 49 | """Operation when the select button is pressed""" 50 | filename = filedialog.asksaveasfilename(initialdir = self.current_dir, confirmoverwrite=False) 51 | if filename and os.path.exists(filename): 52 | filename = os.path.abspath(filename) 53 | self.current_dir = os.path.dirname(filename) 54 | self.filename_var.set(filename) 55 | 56 | def disable(self): 57 | """Disable file_entry field & button""" 58 | self.file_entry['state'] = Tk.DISABLED 59 | self.file_select['state'] = Tk.DISABLED 60 | 61 | def enable(self): 62 | """Enable file_entry field & button""" 63 | self.file_entry['state'] = Tk.NORMAL 64 | self.file_select['state'] = Tk.NORMAL 65 | 66 | def get_filename(self): 67 | """Get a file name 68 | 69 | Acquire the file name entered in the file_entry field. 70 | 71 | Returns 72 | ---------- 73 | string file name. But it may be an invalid file name. 74 | '' (No file_entry) 75 | """ 76 | get_name = self.filename_var.get() 77 | return(get_name) 78 | -------------------------------------------------------------------------------- /demo/M5StackATOM/GUI/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import tkinter as Tk 4 | from mainframe import MainFrame 5 | 6 | root = Tk.Tk() 7 | root.title("infrared recorder") 8 | root.geometry("720x700") 9 | root.minsize(640, 640) 10 | root.maxsize(800, 780) 11 | root.option_add('*font', ('fixed', 12)) 12 | sub_frame = MainFrame(root) 13 | sub_frame.pack() 14 | root.mainloop() 15 | -------------------------------------------------------------------------------- /demo/M5StackATOM/GUI/mainframe.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import tkinter as Tk 4 | from fileframe import FileFrame 5 | from dataframe import DataFrame 6 | from signalframe import SignalFrame 7 | 8 | class MainFrame(Tk.Frame): 9 | """Integrated class for all frames""" 10 | def __init__(self, master, **key): 11 | super().__init__(master, **key) 12 | self.file_frame = FileFrame(self) 13 | self.data_frame = DataFrame(self) 14 | self.signal_frame = SignalFrame(self) 15 | self.file_frame.pack(fill=Tk.X, padx=10, pady=5) 16 | self.data_frame.pack(side=Tk.LEFT, padx=10, pady=5) 17 | self.signal_frame.pack(side=Tk.LEFT, padx=10, pady=5) 18 | 19 | # Processing across frames 20 | def get_filename(self): 21 | return(self.file_frame.get_filename()) 22 | 23 | def file_lock(self): 24 | self.file_frame.disable() 25 | 26 | def file_unlock(self): 27 | self.file_frame.enable() 28 | 29 | def signal_disable(self): 30 | self.signal_frame.disable() 31 | 32 | def signal_enable(self, signal_list: list, comment_text: str): 33 | self.signal_frame.enable(signal_list, comment_text) 34 | 35 | def data_update(self, signal_list: list, comment_text: str): 36 | self.data_frame.update(signal_list, comment_text) 37 | -------------------------------------------------------------------------------- /demo/M5StackATOM/GUI/signalframe.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import tkinter as Tk 4 | from tkinter.scrolledtext import ScrolledText 5 | from tkinter import messagebox 6 | from communication import Communication 7 | 8 | class SignalFrame(Tk.Frame): 9 | """Class of signal transmission, reception management 10 | 11 | Get the waveform of the infrared demodulation IC output. 12 | It is also possible to transmit the acquired waveform. 13 | 14 | Waveform 15 | ---------- 16 | _______ ______ ____ <-Blank time -> 17 | __| |____| |________| |___________________ 18 | start Considered as signal end 19 | <- t0 ->< t1 >< t2 ><-- t3 --> ... [usec] 20 | 21 | Data 22 | ---------- 23 | [t0, t1, t2, t3, t4] (The number of elements will be odd.) 24 | 25 | Signal end 26 | ---------- 27 | 1. If the size is specified, it is limited by the number of 28 | elements in the above data. 29 | 2. If the idle time exceeds the specified time after the 30 | start of reception, it is regarded as the end of reception. 31 | 32 | Feature 33 | ---------- 34 | The output of the infrared demodulation IC cannot restore 35 | the correct transmission waveform due to processing delay. 36 | This waveform delay is corrected on the device processing 37 | side(ESP32) by micropython. 38 | """ 39 | # Widget size 40 | WIDTH_SIGNAL = 14 41 | HEIGHT_SIGNAL = 20 42 | WIDTH_COMMENT = 14 43 | HEIGHT_COMMENT = 8 44 | WIDTH_ENTRY = 5 45 | # background color 46 | COLOR_GREEN = '#98FB98' 47 | COLOR_PINK = '#FFB6C1' 48 | 49 | def __init__(self, master, **key): 50 | """Initialize this sub frame 51 | 52 | Parameters 53 | ---------- 54 | master : Parent frame 55 | 56 | GUI 57 | ---------- 58 | 59 | ************** ************** 60 | frame0 frame1 61 | ************** ************** 62 | 63 | Signal 64 | --------------- -- Stop condition ------- 65 | | 0001: 4000 | | _______ | 66 | | 0002: 800 | | Wait [sec] | | | 67 | | ... | | _______ | 68 | | ... | | Size | | | 69 | --------------- | _______ | 70 | | Blank[msec] | | | 71 | Comment -------------------------- 72 | --------------- 73 | | This is a | 74 | | comment | ************** 75 | | | frame2 76 | | | ************** 77 | --------------- ________ 78 | | Record | 79 | ________ 80 | | Commit | 81 | ________ 82 | | Cancel | 83 | ________ 84 | | Send | 85 | 86 | *************** 87 | Widget 88 | *************** 89 | frame0 90 | self.signal (ScrolledText) 91 | self.comment (ScrolledText) 92 | frame1 93 | self.wait (Entry) 94 | self.wait_var (IntVar) 95 | self.size (Entry) 96 | self.size_var (IntVar) 97 | self.blank (Entry) 98 | self.blank_var (IntVar) 99 | frame2 100 | self.record_button (Button) 101 | self.commit_button (Button) 102 | self.cancel_button (Button) 103 | self.send_button (Button) 104 | """ 105 | super().__init__(master, **key) 106 | self.mastar = master 107 | # inner frame 108 | self.inner_frame0 = Tk.Frame(self) 109 | self.inner_frame1 = Tk.LabelFrame(self, text='Stop condition') 110 | self.inner_frame2 = Tk.Frame(self) 111 | # variable 112 | self.wait_var = Tk.IntVar(value=3) # [sec] 113 | self.size_var = Tk.IntVar(value=1023) 114 | self.blank_var = Tk.IntVar(value=200) # [msec] 115 | # widget in frame0 116 | Tk.Label(self.inner_frame0, text='Signal').pack(anchor=Tk.W) 117 | self.signal = ScrolledText(self.inner_frame0, 118 | bg=SignalFrame.COLOR_GREEN, 119 | width=SignalFrame.WIDTH_SIGNAL, 120 | height=SignalFrame.HEIGHT_SIGNAL) 121 | self.signal['state'] = Tk.DISABLED 122 | self.signal.pack(fill=Tk.X, padx=10, pady=5) 123 | 124 | Tk.Label(self.inner_frame0, text='Comment').pack(anchor=Tk.W) 125 | self.comment = ScrolledText(self.inner_frame0, 126 | bg=SignalFrame.COLOR_GREEN, 127 | width=SignalFrame.WIDTH_COMMENT, 128 | height=SignalFrame.HEIGHT_COMMENT) 129 | self.comment.bind("", self._on_modified) 130 | self.comment['state'] = Tk.DISABLED 131 | self.comment.pack(fill=Tk.X, padx=10, pady=5) 132 | 133 | # widget in frame1 134 | Tk.Label(self.inner_frame1, text='Wait [sec]').grid(column=0, row=0, sticky=Tk.W, padx=10, pady=5) 135 | Tk.Label(self.inner_frame1, text='Size').grid(column=0, row=1, sticky=Tk.W, padx=10, pady=5) 136 | Tk.Label(self.inner_frame1, text='Blank [msec]').grid(column=0, row=2, sticky=Tk.W, padx=10, pady=5) 137 | self.wait = Tk.Entry(self.inner_frame1, 138 | textvariable=self.wait_var, 139 | width=SignalFrame.WIDTH_ENTRY) 140 | self.wait.grid(column=1, row=0, sticky=Tk.EW, padx=10, pady=5) 141 | self.size = Tk.Entry(self.inner_frame1, 142 | textvariable=self.size_var, 143 | width=SignalFrame.WIDTH_ENTRY) 144 | self.size.grid(column=1, row=1, sticky=Tk.EW, padx=10, pady=5) 145 | self.blank = Tk.Entry(self.inner_frame1, 146 | textvariable=self.blank_var, 147 | width=SignalFrame.WIDTH_ENTRY) 148 | self.blank.grid(column=1, row=2, sticky=Tk.EW, padx=10, pady=5) 149 | 150 | # widget in frame2 151 | self.record_button = Tk.Button(self.inner_frame2, 152 | text='Record', 153 | command=self._record) 154 | self.record_button['state'] = Tk.DISABLED 155 | self.record_button.pack(fill=Tk.X, padx=10, pady=5) 156 | 157 | self.commit_button = Tk.Button(self.inner_frame2, 158 | text='Commit', 159 | command=self._commit) 160 | self.commit_button['state'] = Tk.DISABLED 161 | self.commit_button.pack(fill=Tk.X, padx=10, pady=5) 162 | 163 | self.cancel_button = Tk.Button(self.inner_frame2, 164 | text='Cancel', 165 | command=self._cancel) 166 | self.cancel_button['state'] = Tk.DISABLED 167 | self.cancel_button.pack(fill=Tk.X, padx=10, pady=5) 168 | 169 | self.send_button = Tk.Button(self.inner_frame2, 170 | text='Send', 171 | command=self._send) 172 | self.send_button['state'] = Tk.DISABLED 173 | self.send_button.pack(fill=Tk.X, padx=10, pady=5) 174 | 175 | # pack inner frame 176 | self.inner_frame0.pack(fill=Tk.X, side=Tk.LEFT, padx=10, pady=10) 177 | self.inner_frame1.pack(fill=Tk.X, side=Tk.TOP, padx=10, pady=15) 178 | self.inner_frame2.pack(fill=Tk.X, side=Tk.TOP, padx=10, pady=15) 179 | 180 | self.signal_modified = False 181 | self.comment_modified = False 182 | self.signal_original = [] 183 | self.signal_override = [] 184 | self.comment_original = '' 185 | 186 | self._communication = Communication() 187 | 188 | def _on_modified(self, event): 189 | """Check if the comment section has been updated""" 190 | if self.comment['state'] == Tk.NORMAL and self.comment_modified is False: 191 | self.comment_modified = True 192 | self.comment['bg'] = SignalFrame.COLOR_PINK 193 | self.commit_button['state'] = Tk.NORMAL 194 | self.cancel_button['state'] = Tk.NORMAL 195 | 196 | def _record(self): 197 | """Operation when the record button is pressed""" 198 | self._edit_off() 199 | _wait = self.wait_var.get() 200 | _timeout = _wait + 2 201 | _size = self.size_var.get() 202 | _blank = self.blank_var.get() 203 | if not self._communication.is_connect(): 204 | if not self._communication.connect(): 205 | messagebox.showerror('Error', "Can't connect device.") 206 | self._edit_on() 207 | return 208 | if 60 < _wait or 1023 < _size or _wait*1000 < _blank: 209 | messagebox.showerror('Error', 'Stop condition is invalid.') 210 | self._edit_on() 211 | return 212 | command = 'r[{},{},{}]\r\n'.format(_wait*1000, _blank, _size) 213 | ack = self._communication.record(command, _timeout) 214 | if ack[0]: 215 | self.signal_modified = True 216 | self.signal_override = ack[1] 217 | self._override_signal(self.signal_override) 218 | else: 219 | messagebox.showerror('Error', "Can't connect device.") 220 | self._edit_on() 221 | 222 | def _commit(self): 223 | """Operation when the commit button is pressed""" 224 | if self.signal_modified: 225 | self.signal_original = self.signal_override 226 | if self.comment_modified: 227 | self.comment_original = self._get_comment() 228 | self.enable(self.signal_original, self.comment_original) 229 | self.master.data_update(self.signal_original, self.comment_original) 230 | 231 | def _cancel(self): 232 | """Operation when the cancel button is pressed""" 233 | self.enable(self.signal_original, self.comment_original) 234 | 235 | def _send(self): 236 | """Operation when the send button is pressed""" 237 | self._edit_off() 238 | _timeout = 5 239 | if not self._communication.is_connect(): 240 | if not self._communication.connect(): 241 | messagebox.showerror('Error', "Can't connect device.") 242 | self._edit_on() 243 | return 244 | if self.signal_modified: 245 | if self.signal_override: 246 | command = 'w{}\r\n'.format(self.signal_override) 247 | else: 248 | command = '' 249 | else: 250 | if self.signal_original: 251 | command = 'w{}\r\n'.format(self.signal_original) 252 | else: 253 | command = '' 254 | if command: 255 | if not self._communication.send(command, _timeout): 256 | messagebox.showerror('Error', "Can't connect device.") 257 | self._edit_on() 258 | 259 | def _edit_off(self): 260 | self.record_button['state'] = Tk.DISABLED 261 | self.commit_button['state'] = Tk.DISABLED 262 | self.cancel_button['state'] = Tk.DISABLED 263 | self.send_button['state'] = Tk.DISABLED 264 | self.comment['state'] = Tk.DISABLED 265 | 266 | def _edit_on(self): 267 | self.record_button['state'] = Tk.NORMAL 268 | if self.signal_modified or self.comment_modified: 269 | self.commit_button['state'] = Tk.NORMAL 270 | self.cancel_button['state'] = Tk.NORMAL 271 | else: 272 | self.commit_button['state'] = Tk.DISABLED 273 | self.cancel_button['state'] = Tk.DISABLED 274 | self.send_button['state'] = Tk.NORMAL 275 | self.comment['state'] = Tk.NORMAL 276 | 277 | def _preset_signal(self, signal_list: list=[]): 278 | self.signal['state'] = Tk.NORMAL 279 | self.signal['bg'] = SignalFrame.COLOR_GREEN 280 | self.signal.delete('1.0', 'end') 281 | self.signal_original = signal_list 282 | self.signal_modified = False 283 | signal_text = '' 284 | for i,content in enumerate(self.signal_original): 285 | signal_text += '{:0>4d}: {:>7,d}\n'.format(i+1, content) 286 | self.signal.insert('1.0', signal_text) 287 | self.signal['state'] = Tk.DISABLED 288 | 289 | def _override_signal(self, signal_list: list): 290 | self.signal['state'] = Tk.NORMAL 291 | self.signal['bg'] = SignalFrame.COLOR_PINK 292 | self.signal.delete('1.0', 'end') 293 | self.signal_override = signal_list.copy() 294 | self.signal_modified = True 295 | signal_text = '' 296 | for i,content in enumerate(self.signal_override): 297 | signal_text += '{:0>4d}: {:>7,d}\n'.format(i+1, content) 298 | self.signal.insert('1.0', signal_text) 299 | self.signal['state'] = Tk.DISABLED 300 | 301 | def _get_comment(self): 302 | return(self.comment.get('1.0', 'end-1c')) 303 | 304 | def disable(self): 305 | """Disable all widgets""" 306 | self.signal['state'] = Tk.NORMAL 307 | # 1-line, 0-column to end 308 | self.signal.delete('1.0', 'end') 309 | self.signal['bg'] = SignalFrame.COLOR_GREEN 310 | self.signal['state'] = Tk.DISABLED 311 | self.comment['state'] = Tk.NORMAL 312 | self.comment.delete('1.0', 'end') 313 | self.comment['bg'] = SignalFrame.COLOR_GREEN 314 | self.comment['state'] = Tk.DISABLED 315 | self.record_button['state'] = Tk.DISABLED 316 | self.commit_button['state'] = Tk.DISABLED 317 | self.cancel_button['state'] = Tk.DISABLED 318 | self.send_button['state'] = Tk.DISABLED 319 | 320 | def enable(self, signal_list: list, comment_text: str): 321 | """Enable all widgets & Initialize""" 322 | self.signal_modified = False 323 | self.comment_modified = False 324 | self._preset_signal(signal_list) 325 | self.comment_original = comment_text 326 | 327 | self.comment['state'] = Tk.NORMAL 328 | self.comment['bg'] = SignalFrame.COLOR_GREEN 329 | self.comment.delete('1.0', 'end') 330 | self.comment.insert('1.0', comment_text) 331 | 332 | self.record_button['state'] = Tk.NORMAL 333 | self.commit_button['state'] = Tk.DISABLED 334 | self.cancel_button['state'] = Tk.DISABLED 335 | self.send_button['state'] = Tk.NORMAL 336 | -------------------------------------------------------------------------------- /demo/M5StackATOM/micropython/main.py: -------------------------------------------------------------------------------- 1 | from machine import Pin 2 | from micropython import const 3 | from gc import collect 4 | import json 5 | from UpyIrTx import UpyIrTx 6 | from UpyIrRx import UpyIrRx 7 | 8 | # Grove pins connected to M5Stack IR unit 9 | _GROVE_PIN = {'ATOM': (32, 26), 10 | 'CORE2': (33, 32), 11 | 'BASIC': (22, 21), 12 | 'GRAY': (22, 21), 13 | 'FIRE': (22, 21), 14 | 'GO': (22, 21), 15 | 'Stick': (33, 32), 16 | 'Else': (32, 12)} 17 | _DEVICE = 'ATOM' 18 | _TX_IDLE_LEVEL = const(0) 19 | _TX_FREQ = const(38000) 20 | _TX_DUTY = const(30) 21 | _RX_IDLE_LEVEL = const(1) 22 | _RX_SIZE = const(1023) 23 | 24 | rx_pin = Pin(_GROVE_PIN[_DEVICE][0], Pin.IN) 25 | rx = UpyIrRx(rx_pin, _RX_SIZE, _RX_IDLE_LEVEL) 26 | 27 | tx_pin = Pin(_GROVE_PIN[_DEVICE][1], Pin.OUT) 28 | tx = UpyIrTx(0, tx_pin, _TX_FREQ, _TX_DUTY, _TX_IDLE_LEVEL) 29 | 30 | cmd = input() 31 | while cmd != 'q': 32 | if len(cmd) > 0: 33 | if cmd[0] == 'r': 34 | # ex. cmd: 'r[3000, 200, 1023] 35 | try: 36 | _wait, _blank, _size = json.loads(cmd[1:]) 37 | if rx.record(_wait, _blank, _size) == UpyIrRx.ERROR_NONE: 38 | print(rx.get_calibrate_list()) 39 | else: 40 | print('[]') 41 | except: 42 | print('[]') 43 | elif cmd[0] == 'w': 44 | # ex. cmd: 'w[420, 1260, 420, ...] 45 | try: 46 | if tx.send(json.loads(cmd[1:])): 47 | print('OK') 48 | else: 49 | print('NG') 50 | except: 51 | print('NG') 52 | else: 53 | print('NG') 54 | del cmd 55 | collect() 56 | cmd = input() 57 | -------------------------------------------------------------------------------- /demo/RP2040/GUI/communication.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import serial 4 | from serial.tools import list_ports 5 | import time 6 | import json 7 | 8 | class Communication(): 9 | """Class to communicate with the device 10 | 11 | The communication path of this class is USB-UART. 12 | Separate the communication path from the GUI. 13 | """ 14 | # M5Stack ATOM (LITE) 15 | #_DEFAULT_VID = 1027 16 | #_DEFAULT_PID = 24577 17 | 18 | # M5Stack Core2 19 | # _DEFAULT_VID = 4292 20 | # _DEFAULT_PID = 60000 21 | 22 | # RaspberryPi pico 23 | _DEFAULT_VID = 11914 24 | _DEFAULT_PID = 5 25 | 26 | def __init__(self, device_name: str='', vid: int=0, pid: int=0): 27 | """Initialize 28 | 29 | Parameters 30 | ---------- 31 | device_name: str 32 | USB device name (ex. 'COM12' in Windows, '/dev/ttyUSB0' in Linux) 33 | For the empty string, the following two parameters are valid. 34 | vid: int 35 | Vendor ID of USB device. If 0, the default value _DEFAULT_VID is applied. 36 | If the device_name parameter is not an empty string, it does not apply. 37 | pid: int 38 | Product ID of USB device If 0, the default value _DEFAULT_PID is applied. 39 | If the device_name parameter is not an empty string, it does not apply. 40 | """ 41 | if device_name: 42 | self._device = device_name 43 | else: 44 | self._device = None 45 | self.connect('', vid, pid) 46 | 47 | def connect(self, device_name: str='', vid: int= 0, pid: int = 0) -> bool: 48 | """Connection 49 | 50 | Disable the currently connected device and connect to the new device. 51 | The device is active only at the moment of sending and receiving data. 52 | Therefore, it is not necessary to connect after disconnecting. 53 | 54 | Parameters 55 | ---------- 56 | device_name: str 57 | USB device name (ex. 'COM12' in Windows, '/dev/ttyUSB0' in Linux) 58 | For the empty string, the following two parameters are valid. 59 | vid: int 60 | Vendor ID of USB device. If 0, the default value _DEFAULT_VID is applied. 61 | If the device_name parameter is not an empty string, it does not apply. 62 | pid: int 63 | Product ID of USB device If 0, the default value _DEFAULT_PID is applied. 64 | If the device_name parameter is not an empty string, it does not apply. 65 | """ 66 | if device_name: 67 | self._device = device_name 68 | return(True) 69 | devlis = list_ports.comports() 70 | if vid <= 0 or pid <= 0: 71 | _vid = Communication._DEFAULT_VID 72 | _pid = Communication._DEFAULT_PID 73 | else: 74 | _vid = vid 75 | _pid = pid 76 | for i in devlis: 77 | if i.vid == _vid and i.pid == _pid: 78 | self._device = i.device 79 | return(True) 80 | self._device = None 81 | return(False) 82 | 83 | def disconnect(self) -> None: 84 | """Disconnect 85 | 86 | Disable the communication path. 87 | """ 88 | self._device = None 89 | 90 | def is_connect(self) -> bool: 91 | """Whether it is connected 92 | 93 | Returns 94 | ---------- 95 | bool 96 | """ 97 | if self._device: 98 | return(True) 99 | else: 100 | return(False) 101 | 102 | def send(self, msg: str, timeout: float=2) -> bool: 103 | """Turn on the infrared signal 104 | 105 | Parameters 106 | ---------- 107 | msg: str 108 | Data sent to the device. The format is 109 | "w[400, 1200, 400, ...]\r\n" 110 | 111 | Returns 112 | ---------- 113 | bool 114 | Whether communication was successful. 115 | """ 116 | if not self.is_connect(): 117 | return(False) 118 | try: 119 | with serial.Serial(self._device, baudrate=115200, timeout=timeout, 120 | write_timeout=2) as ser: 121 | ser.reset_input_buffer() 122 | ser.reset_output_buffer() 123 | ser.write(msg.encode()) 124 | start_time = time.time() 125 | # Detect echo back 126 | ack = b'' 127 | while not ack: 128 | ack = ser.readline() 129 | if time.time() - start_time > timeout: 130 | raise Exception() 131 | # Detect ack 132 | ack = b'' 133 | while not ack: 134 | ack = ser.readline() 135 | if time.time() - start_time > timeout: 136 | raise Exception() 137 | # Wait a moment before disconnecting. 138 | time.sleep(0.5) 139 | if b'OK' in ack: 140 | return(True) 141 | else: 142 | return(False) 143 | except: 144 | self.disconnect() 145 | return(False) 146 | 147 | def record(self, msg: str, timeout: float=4) -> tuple: 148 | """Get infrared received signal 149 | 150 | Parameters 151 | ---------- 152 | msg: str 153 | Data sent to the device. The format is 154 | "r[4000, 200, 1023]\r\n" 155 | The first factor is the reception timeout time [msec] 156 | The second element is the static duration [msec] 157 | that recognizes the end of reception. 158 | The third factor is the upper limit of the received signal length. 159 | 160 | Returns 161 | ---------- 162 | tuple (item1, item2) 163 | item1: bool 164 | Communication error 165 | item2: list 166 | Integer list meaning received signal. 167 | If it fails, an empty list is returned. 168 | """ 169 | if not self.is_connect(): 170 | return((False, [])) 171 | try: 172 | with serial.Serial(self._device, baudrate=115200, timeout=timeout, 173 | write_timeout=2) as ser: 174 | ser.reset_input_buffer() 175 | ser.reset_output_buffer() 176 | ser.write(msg.encode()) 177 | start_time = time.time() 178 | # Detect echo back 179 | ack = b'' 180 | while not ack: 181 | ack = ser.readline() 182 | if time.time() - start_time > timeout: 183 | raise Exception() 184 | # Detect ack 185 | ack = b'' 186 | while not ack: 187 | ack = ser.readline() 188 | if time.time() - start_time > timeout: 189 | raise Exception() 190 | # Wait a moment before disconnecting. 191 | time.sleep(0.5) 192 | return([True, json.loads(ack)]) 193 | except: 194 | self.disconnect() 195 | return((False, [])) 196 | -------------------------------------------------------------------------------- /demo/RP2040/GUI/dataframe.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import tkinter as Tk 4 | from tkinter import messagebox 5 | import json 6 | import os 7 | 8 | from numpy import isin 9 | 10 | class ScrolledListbox(Tk.Listbox): 11 | """ Listbox widget with vertical scroll bar""" 12 | def __init__(self, master, **key): 13 | """Initialize extended widget "ScrolledListbox" 14 | 15 | inputs 16 | ---------- 17 | master : Parent frame 18 | key : option dictionary 19 | """ 20 | self.yscroll = Tk.Scrollbar(master, orient=Tk.VERTICAL) 21 | self.yscroll.pack(side=Tk.RIGHT, fill=Tk.Y, expand=1) 22 | key['yscrollcommand']=self.yscroll.set 23 | super().__init__(master, **key) 24 | self.pack(side=Tk.LEFT, fill=Tk.BOTH, expand=1) 25 | self.yscroll.config(command=self.yview) 26 | 27 | class DataFrame(Tk.Frame): 28 | """Class of control data 29 | 30 | When remote controler data is registered, data management and 31 | display control are performed. 32 | 33 | Data format(dictionary) 34 | ---------- 35 | {'action key1': {'signal': [100, 20, ...], 'comment': 'xxx'}, 36 | 'action key2': {'signal': [360, 90, ...], 'comment': 'yyy'}, 37 | .... 38 | '__system__': {'signal': [], 'comment': 'meta comment'} 39 | } 40 | 41 | Remote control data consists of time sequence data and 42 | annotations with action (CH1, REC, etc...) as a key. 43 | 44 | The action key is displayed in a list. The current choice 45 | of the list is automatically identified. 46 | 47 | A special action key '__system__' means a comment for this entire data. 48 | This key always exists and cannot be erased. 49 | """ 50 | # Widget size 51 | HEIGHT_KEYLIST = 10 52 | WIDTH_KEYLIST = 18 53 | # Special key name 54 | SYSTEM_KEY = '__system__' 55 | # State 56 | STATE_CLOSED = 0 57 | STATE_OPENED = 1 58 | STATE_EDITED = 2 59 | 60 | def __init__(self, master, **key): 61 | """Initialize sub frame for data-control 62 | 63 | 1. Register the remote control data read from the file. 64 | 2. Data management, information update, and information 65 | display are performed. 66 | 67 | Parameters 68 | ---------- 69 | master : Parent frame 70 | key : option dictionary 71 | 72 | GUI Widget 73 | ---------- ---------- 74 | ________ 75 | | Open | self.open_button (Button) 76 | ________ 77 | | Save | self.save_button (Button) 78 | ________ 79 | | Close | self.close_button (Button) 80 | 81 | key 82 | ----------- 83 | | | self.key_list (ScrolledListbox) 84 | | | 85 | | | 86 | | | 87 | ----------- 88 | 89 | Select key 90 | ___________ 91 | | | self.select_key (Entry) 92 | ___________ self.select_var (StringVar) 93 | | Delete | self.delete_button (Button) 94 | 95 | Edit key 96 | ___________ 97 | | | self.edit_key (Entry) 98 | ___________ self.edit_var (StringVar) 99 | | Append | self.append_button (Button) 100 | ___________ 101 | | Rename | self.rename_button (Button) 102 | 103 | """ 104 | super().__init__(master, **key) 105 | self.master = master 106 | # inner frame 107 | self.inner_frame0 = Tk.Frame(self) 108 | self.inner_frame1 = Tk.Frame(self) 109 | self.inner_frame2 = Tk.Frame(self) 110 | # variable 111 | self.select_var = Tk.StringVar(value='') 112 | self.edit_var = Tk.StringVar(value='') 113 | 114 | # widget in frame0 115 | self.open_button = Tk.Button(self.inner_frame0, 116 | text='Open', 117 | command=self._open) 118 | self.open_button['state'] = Tk.NORMAL 119 | self.save_button = Tk.Button(self.inner_frame0, 120 | text='Save', 121 | command=self._save) 122 | self.save_button['state'] = Tk.DISABLED 123 | self.close_button = Tk.Button(self.inner_frame0, 124 | text='Close', 125 | command=self._close) 126 | self.close_button['state'] = Tk.DISABLED 127 | self.open_button.pack(fill=Tk.X, padx=10, pady=5) 128 | self.save_button.pack(fill=Tk.X, padx=10, pady=5) 129 | self.close_button.pack(fill=Tk.X, padx=10, pady=5) 130 | 131 | # widget in frame1 132 | Tk.Label(self.inner_frame1, text='Key list').pack(anchor=Tk.W) 133 | self.key_list = ScrolledListbox(self.inner_frame1, 134 | selectmode=Tk.SINGLE, 135 | height=DataFrame.HEIGHT_KEYLIST, 136 | width=DataFrame.WIDTH_KEYLIST) 137 | self.key_list.pack(fill=Tk.X, padx=10, pady=5) 138 | 139 | # widget in frame2 140 | Tk.Label(self.inner_frame2, text='Select key').pack(anchor=Tk.W) 141 | self.select_key = Tk.Label(self.inner_frame2, 142 | textvariable=self.select_var, 143 | relief='ridge') 144 | self.select_key.pack(fill=Tk.X, padx=10, pady=5) 145 | self.delete_button = Tk.Button(self.inner_frame2, 146 | text='Delete', 147 | command=self._delete) 148 | self.delete_button['state'] = Tk.DISABLED 149 | self.delete_button.pack(fill=Tk.X, padx=10, pady=5) 150 | 151 | Tk.Label(self.inner_frame2, text='Edit key').pack(anchor=Tk.W) 152 | self.edit_key = Tk.Entry(self.inner_frame2, 153 | textvariable=self.edit_var) 154 | self.edit_key['state'] = Tk.DISABLED 155 | self.edit_key.pack(fill=Tk.X, padx=10, pady=5) 156 | self.append_button = Tk.Button(self.inner_frame2, 157 | text='Append', 158 | command=self._append) 159 | self.append_button['state'] = Tk.DISABLED 160 | self.append_button.pack(fill=Tk.X, padx=10, pady=5) 161 | self.rename_button = Tk.Button(self.inner_frame2, 162 | text='Rename', 163 | command=self._rename) 164 | self.rename_button['state'] = Tk.DISABLED 165 | self.rename_button.pack(fill=Tk.X, padx=10, pady=5) 166 | 167 | # Register callback when list is selected 168 | self.key_list.bind('<>', self._key_list_select) 169 | # pack inner frame 170 | self.inner_frame0.pack(fill=Tk.X, padx=10, pady=5) 171 | self.inner_frame1.pack(fill=Tk.X, padx=10, pady=5) 172 | self.inner_frame2.pack(fill=Tk.X, padx=10, pady=5) 173 | # data 174 | self.save_data = {DataFrame.SYSTEM_KEY: {'signal':[], 'comment': ''}} 175 | self.save_keys = [DataFrame.SYSTEM_KEY] 176 | self.select_index = 0 177 | self.state = DataFrame.STATE_CLOSED 178 | 179 | def _key_list_select(self, event): 180 | """Callback when list is selected""" 181 | widget = event.widget 182 | tpl_select = widget.curselection() 183 | if(tpl_select): 184 | self.select_index = tpl_select[0] 185 | self._view_information() 186 | 187 | def _open(self): 188 | """Operation when the open button is pressed""" 189 | try: 190 | filename = self.master.get_filename() 191 | if os.path.isfile(filename): 192 | with open(filename) as fp: 193 | self.save_data = json.load(fp) 194 | # Type check 195 | if not isinstance(self.save_data, dict): 196 | raise Exception() 197 | for key in self.save_data: 198 | if not isinstance(self.save_data[key], dict): 199 | raise Exception() 200 | else: 201 | self.save_data = {DataFrame.SYSTEM_KEY: {'signal':[], 'comment': ''}} 202 | self.save_keys = list(self.save_data.keys()) 203 | if DataFrame.SYSTEM_KEY not in self.save_keys: 204 | self.save_data[DataFrame.SYSTEM_KEY] = {'signal': [], 'comment': ''} 205 | else: 206 | self.save_keys.remove(DataFrame.SYSTEM_KEY) 207 | self.save_keys.sort() 208 | self.save_keys.insert(0, DataFrame.SYSTEM_KEY) 209 | except: 210 | messagebox.showerror('Error', "Can't open file or Data format is illegal.") 211 | return 212 | self.key_list.delete(0, Tk.END) 213 | self.key_list.insert(Tk.END, *self.save_keys) 214 | self.select_index = 0 215 | self.key_list.selection_set(self.select_index) 216 | self._view_information() 217 | 218 | self.open_button['state'] = Tk.DISABLED 219 | self.save_button['state'] = Tk.DISABLED 220 | self.close_button['state'] = Tk.NORMAL 221 | self.master.file_lock() 222 | self.state = DataFrame.STATE_OPENED 223 | self.delete_button['state'] = Tk.NORMAL 224 | self.edit_key['state'] = Tk.NORMAL 225 | self.append_button['state'] = Tk.NORMAL 226 | self.rename_button['state'] = Tk.NORMAL 227 | 228 | def _save(self): 229 | """Operation when the save button is pressed""" 230 | try: 231 | with open(self.master.get_filename(), mode='w') as fp: 232 | json.dump(self.save_data, fp) 233 | except: 234 | messagebox.showerror('Error', "Can't save safely.") 235 | return 236 | self.save_button['state'] = Tk.DISABLED 237 | self.state = DataFrame.STATE_OPENED 238 | 239 | def _close(self): 240 | """Operation when the close button is pressed""" 241 | if self.state == DataFrame.STATE_EDITED: 242 | ack = messagebox.askyesno('Data has been updated', 'Do you want to close without saving?') 243 | if ack is False: 244 | return 245 | self.open_button['state'] = Tk.NORMAL 246 | self.save_button['state'] = Tk.DISABLED 247 | self.close_button['state'] = Tk.DISABLED 248 | self.select_var.set('') 249 | self.key_list.delete(0, Tk.END) 250 | self.delete_button['state'] = Tk.DISABLED 251 | self.edit_key['state'] = Tk.NORMAL 252 | self.edit_var.set('') 253 | self.edit_key['state'] = Tk.DISABLED 254 | self.append_button['state'] = Tk.DISABLED 255 | self.rename_button['state'] = Tk.DISABLED 256 | self.save_data = {} 257 | self.select_index = 0 258 | self.state = DataFrame.STATE_CLOSED 259 | self.master.signal_disable() 260 | self.master.file_unlock() 261 | 262 | def _delete(self): 263 | """Operation when the delete button is pressed""" 264 | delete_key = self.save_keys[self.select_index] 265 | if delete_key == DataFrame.SYSTEM_KEY: 266 | return 267 | del self.save_data[delete_key] 268 | del self.save_keys[self.select_index] 269 | self.key_list.delete(self.select_index) 270 | if len(self.save_keys) <= self.select_index: 271 | self.select_index -= 1 272 | self.key_list.select_clear(0, Tk.END) 273 | self.key_list.select_set(self.select_index) 274 | self.state = DataFrame.STATE_EDITED 275 | self.save_button['state'] = Tk.NORMAL 276 | self._view_information() 277 | 278 | def _append(self): 279 | """Operation when the append button is pressed""" 280 | append_key = self.edit_var.get() 281 | if append_key == '' or append_key in self.save_keys: 282 | return 283 | self.save_data[append_key] = {'signal': [], 'comment': ''} 284 | self.save_keys.append(append_key) 285 | self.key_list.insert(Tk.END, append_key) 286 | self.select_index = len(self.save_keys) - 1 287 | self.key_list.select_clear(0, Tk.END) 288 | self.key_list.select_set(self.select_index) 289 | self.state = DataFrame.STATE_EDITED 290 | self.save_button['state'] = Tk.NORMAL 291 | self._view_information() 292 | 293 | def _rename(self): 294 | """Operation when the rename button is pressed""" 295 | now_key = self.save_keys[self.select_index] 296 | rename_key = self.edit_var.get() 297 | if rename_key == '' or rename_key in self.save_keys\ 298 | or now_key == DataFrame.SYSTEM_KEY: 299 | return 300 | self.save_data[rename_key] = self.save_data[now_key] 301 | del self.save_data[now_key] 302 | self.save_keys[self.select_index] = rename_key 303 | self.key_list.delete(self.select_index) 304 | self.key_list.insert(self.select_index, rename_key) 305 | self.key_list.select_clear(0, Tk.END) 306 | self.key_list.select_set(self.select_index) 307 | self.state = DataFrame.STATE_EDITED 308 | self.save_button['state'] = Tk.NORMAL 309 | self._view_information() 310 | 311 | def _view_information(self): 312 | """Display signal / comment data by signalframe.py""" 313 | self.select_var.set(self.save_keys[self.select_index]) 314 | selected = self.save_data.get(self.save_keys[self.select_index]) 315 | selected_signal = selected.get('signal', []) 316 | selected_comment = selected.get('comment', '') 317 | self.master.signal_enable(selected_signal, selected_comment) 318 | 319 | def update(self, signal_list: list, comment_text: str): 320 | """Data update process from signalframe.py""" 321 | self.save_data[self.save_keys[self.select_index]] =\ 322 | {'signal': signal_list.copy(), 'comment': comment_text} 323 | self.state = DataFrame.STATE_EDITED 324 | self.save_button['state'] = Tk.NORMAL 325 | -------------------------------------------------------------------------------- /demo/RP2040/GUI/fileframe.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import tkinter as Tk 4 | from tkinter import filedialog 5 | import os 6 | 7 | class FileFrame(Tk.Frame): 8 | """Class of file select""" 9 | # Length of character string in the file_entry field 10 | WIDTH_FILENAME = 60 11 | 12 | def __init__(self, master, **key): 13 | """Initialize sub frame for file select 14 | 15 | ********** 16 | GUI 17 | ********** 18 | --------------- -------- 19 | File: | | | Select | 20 | --------------- -------- 21 | 22 | ********** 23 | Widget 24 | ********** 25 | self.file_entry (file_entry) 26 | self.filename_var (StringVar) 27 | self.file_select (Button) 28 | """ 29 | super().__init__(master, **key) 30 | # inner frame 31 | self.inner_frame0 = Tk.Frame(self) 32 | # variable 33 | self.filename_var = Tk.StringVar() 34 | # widget in frame0 35 | Tk.Label(self.inner_frame0, text='File').pack(side=Tk.LEFT) 36 | self.file_entry = Tk.Entry(self.inner_frame0, 37 | textvariable=self.filename_var, 38 | width=FileFrame.WIDTH_FILENAME) 39 | self.file_entry.pack(side=Tk.LEFT, fill=Tk.X, padx=5) 40 | self.file_select = Tk.Button(self.inner_frame0, text='Select', 41 | command=self._file_select) 42 | self.file_select.pack(side=Tk.LEFT, padx=10) 43 | # pack inner frame 44 | self.inner_frame0.pack(fill=Tk.X, padx=10, pady=5) 45 | # others 46 | self.current_dir = os.path.curdir 47 | 48 | def _file_select(self): 49 | """Operation when the select button is pressed""" 50 | filename = filedialog.asksaveasfilename(initialdir = self.current_dir, confirmoverwrite=False) 51 | if filename and os.path.exists(filename): 52 | filename = os.path.abspath(filename) 53 | self.current_dir = os.path.dirname(filename) 54 | self.filename_var.set(filename) 55 | 56 | def disable(self): 57 | """Disable file_entry field & button""" 58 | self.file_entry['state'] = Tk.DISABLED 59 | self.file_select['state'] = Tk.DISABLED 60 | 61 | def enable(self): 62 | """Enable file_entry field & button""" 63 | self.file_entry['state'] = Tk.NORMAL 64 | self.file_select['state'] = Tk.NORMAL 65 | 66 | def get_filename(self): 67 | """Get a file name 68 | 69 | Acquire the file name entered in the file_entry field. 70 | 71 | Returns 72 | ---------- 73 | string file name. But it may be an invalid file name. 74 | '' (No file_entry) 75 | """ 76 | get_name = self.filename_var.get() 77 | return(get_name) 78 | -------------------------------------------------------------------------------- /demo/RP2040/GUI/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import tkinter as Tk 4 | from mainframe import MainFrame 5 | 6 | root = Tk.Tk() 7 | root.title("infrared recorder") 8 | root.geometry("720x700") 9 | root.minsize(640, 640) 10 | root.maxsize(800, 780) 11 | root.option_add('*font', ('fixed', 12)) 12 | sub_frame = MainFrame(root) 13 | sub_frame.pack() 14 | root.mainloop() 15 | -------------------------------------------------------------------------------- /demo/RP2040/GUI/mainframe.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import tkinter as Tk 4 | from fileframe import FileFrame 5 | from dataframe import DataFrame 6 | from signalframe import SignalFrame 7 | 8 | class MainFrame(Tk.Frame): 9 | """Integrated class for all frames""" 10 | def __init__(self, master, **key): 11 | super().__init__(master, **key) 12 | self.file_frame = FileFrame(self) 13 | self.data_frame = DataFrame(self) 14 | self.signal_frame = SignalFrame(self) 15 | self.file_frame.pack(fill=Tk.X, padx=10, pady=5) 16 | self.data_frame.pack(side=Tk.LEFT, padx=10, pady=5) 17 | self.signal_frame.pack(side=Tk.LEFT, padx=10, pady=5) 18 | 19 | # Processing across frames 20 | def get_filename(self): 21 | return(self.file_frame.get_filename()) 22 | 23 | def file_lock(self): 24 | self.file_frame.disable() 25 | 26 | def file_unlock(self): 27 | self.file_frame.enable() 28 | 29 | def signal_disable(self): 30 | self.signal_frame.disable() 31 | 32 | def signal_enable(self, signal_list: list, comment_text: str): 33 | self.signal_frame.enable(signal_list, comment_text) 34 | 35 | def data_update(self, signal_list: list, comment_text: str): 36 | self.data_frame.update(signal_list, comment_text) 37 | -------------------------------------------------------------------------------- /demo/RP2040/GUI/signalframe.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import tkinter as Tk 4 | from tkinter.scrolledtext import ScrolledText 5 | from tkinter import messagebox 6 | from communication import Communication 7 | 8 | class SignalFrame(Tk.Frame): 9 | """Class of signal transmission, reception management 10 | 11 | Get the waveform of the infrared demodulation IC output. 12 | It is also possible to transmit the acquired waveform. 13 | 14 | Waveform 15 | ---------- 16 | _______ ______ ____ <-Blank time -> 17 | __| |____| |________| |___________________ 18 | start Considered as signal end 19 | <- t0 ->< t1 >< t2 ><-- t3 --> ... [usec] 20 | 21 | Data 22 | ---------- 23 | [t0, t1, t2, t3, t4] (The number of elements will be odd.) 24 | 25 | Signal end 26 | ---------- 27 | 1. If the size is specified, it is limited by the number of 28 | elements in the above data. 29 | 2. If the idle time exceeds the specified time after the 30 | start of reception, it is regarded as the end of reception. 31 | 32 | Feature 33 | ---------- 34 | The output of the infrared demodulation IC cannot restore 35 | the correct transmission waveform due to processing delay. 36 | This waveform delay is corrected on the device processing 37 | side(ESP32) by micropython. 38 | """ 39 | # Widget size 40 | WIDTH_SIGNAL = 14 41 | HEIGHT_SIGNAL = 20 42 | WIDTH_COMMENT = 14 43 | HEIGHT_COMMENT = 8 44 | WIDTH_ENTRY = 5 45 | # background color 46 | COLOR_GREEN = '#98FB98' 47 | COLOR_PINK = '#FFB6C1' 48 | 49 | def __init__(self, master, **key): 50 | """Initialize this sub frame 51 | 52 | Parameters 53 | ---------- 54 | master : Parent frame 55 | 56 | GUI 57 | ---------- 58 | 59 | ************** ************** 60 | frame0 frame1 61 | ************** ************** 62 | 63 | Signal 64 | --------------- -- Stop condition ------- 65 | | 0001: 4000 | | _______ | 66 | | 0002: 800 | | Wait [sec] | | | 67 | | ... | | _______ | 68 | | ... | | Size | | | 69 | --------------- | _______ | 70 | | Blank[msec] | | | 71 | Comment -------------------------- 72 | --------------- 73 | | This is a | 74 | | comment | ************** 75 | | | frame2 76 | | | ************** 77 | --------------- ________ 78 | | Record | 79 | ________ 80 | | Commit | 81 | ________ 82 | | Cancel | 83 | ________ 84 | | Send | 85 | 86 | *************** 87 | Widget 88 | *************** 89 | frame0 90 | self.signal (ScrolledText) 91 | self.comment (ScrolledText) 92 | frame1 93 | self.wait (Entry) 94 | self.wait_var (IntVar) 95 | self.size (Entry) 96 | self.size_var (IntVar) 97 | self.blank (Entry) 98 | self.blank_var (IntVar) 99 | frame2 100 | self.record_button (Button) 101 | self.commit_button (Button) 102 | self.cancel_button (Button) 103 | self.send_button (Button) 104 | """ 105 | super().__init__(master, **key) 106 | self.mastar = master 107 | # inner frame 108 | self.inner_frame0 = Tk.Frame(self) 109 | self.inner_frame1 = Tk.LabelFrame(self, text='Stop condition') 110 | self.inner_frame2 = Tk.Frame(self) 111 | # variable 112 | self.wait_var = Tk.IntVar(value=3) # [sec] 113 | self.size_var = Tk.IntVar(value=1023) 114 | self.blank_var = Tk.IntVar(value=200) # [msec] 115 | # widget in frame0 116 | Tk.Label(self.inner_frame0, text='Signal').pack(anchor=Tk.W) 117 | self.signal = ScrolledText(self.inner_frame0, 118 | bg=SignalFrame.COLOR_GREEN, 119 | width=SignalFrame.WIDTH_SIGNAL, 120 | height=SignalFrame.HEIGHT_SIGNAL) 121 | self.signal['state'] = Tk.DISABLED 122 | self.signal.pack(fill=Tk.X, padx=10, pady=5) 123 | 124 | Tk.Label(self.inner_frame0, text='Comment').pack(anchor=Tk.W) 125 | self.comment = ScrolledText(self.inner_frame0, 126 | bg=SignalFrame.COLOR_GREEN, 127 | width=SignalFrame.WIDTH_COMMENT, 128 | height=SignalFrame.HEIGHT_COMMENT) 129 | self.comment.bind("", self._on_modified) 130 | self.comment['state'] = Tk.DISABLED 131 | self.comment.pack(fill=Tk.X, padx=10, pady=5) 132 | 133 | # widget in frame1 134 | Tk.Label(self.inner_frame1, text='Wait [sec]').grid(column=0, row=0, sticky=Tk.W, padx=10, pady=5) 135 | Tk.Label(self.inner_frame1, text='Size').grid(column=0, row=1, sticky=Tk.W, padx=10, pady=5) 136 | Tk.Label(self.inner_frame1, text='Blank [msec]').grid(column=0, row=2, sticky=Tk.W, padx=10, pady=5) 137 | self.wait = Tk.Entry(self.inner_frame1, 138 | textvariable=self.wait_var, 139 | width=SignalFrame.WIDTH_ENTRY) 140 | self.wait.grid(column=1, row=0, sticky=Tk.EW, padx=10, pady=5) 141 | self.size = Tk.Entry(self.inner_frame1, 142 | textvariable=self.size_var, 143 | width=SignalFrame.WIDTH_ENTRY) 144 | self.size.grid(column=1, row=1, sticky=Tk.EW, padx=10, pady=5) 145 | self.blank = Tk.Entry(self.inner_frame1, 146 | textvariable=self.blank_var, 147 | width=SignalFrame.WIDTH_ENTRY) 148 | self.blank.grid(column=1, row=2, sticky=Tk.EW, padx=10, pady=5) 149 | 150 | # widget in frame2 151 | self.record_button = Tk.Button(self.inner_frame2, 152 | text='Record', 153 | command=self._record) 154 | self.record_button['state'] = Tk.DISABLED 155 | self.record_button.pack(fill=Tk.X, padx=10, pady=5) 156 | 157 | self.commit_button = Tk.Button(self.inner_frame2, 158 | text='Commit', 159 | command=self._commit) 160 | self.commit_button['state'] = Tk.DISABLED 161 | self.commit_button.pack(fill=Tk.X, padx=10, pady=5) 162 | 163 | self.cancel_button = Tk.Button(self.inner_frame2, 164 | text='Cancel', 165 | command=self._cancel) 166 | self.cancel_button['state'] = Tk.DISABLED 167 | self.cancel_button.pack(fill=Tk.X, padx=10, pady=5) 168 | 169 | self.send_button = Tk.Button(self.inner_frame2, 170 | text='Send', 171 | command=self._send) 172 | self.send_button['state'] = Tk.DISABLED 173 | self.send_button.pack(fill=Tk.X, padx=10, pady=5) 174 | 175 | # pack inner frame 176 | self.inner_frame0.pack(fill=Tk.X, side=Tk.LEFT, padx=10, pady=10) 177 | self.inner_frame1.pack(fill=Tk.X, side=Tk.TOP, padx=10, pady=15) 178 | self.inner_frame2.pack(fill=Tk.X, side=Tk.TOP, padx=10, pady=15) 179 | 180 | self.signal_modified = False 181 | self.comment_modified = False 182 | self.signal_original = [] 183 | self.signal_override = [] 184 | self.comment_original = '' 185 | 186 | self._communication = Communication() 187 | 188 | def _on_modified(self, event): 189 | """Check if the comment section has been updated""" 190 | if self.comment['state'] == Tk.NORMAL and self.comment_modified is False: 191 | self.comment_modified = True 192 | self.comment['bg'] = SignalFrame.COLOR_PINK 193 | self.commit_button['state'] = Tk.NORMAL 194 | self.cancel_button['state'] = Tk.NORMAL 195 | 196 | def _record(self): 197 | """Operation when the record button is pressed""" 198 | self._edit_off() 199 | _wait = self.wait_var.get() 200 | _timeout = _wait + 2 201 | _size = self.size_var.get() 202 | _blank = self.blank_var.get() 203 | if not self._communication.is_connect(): 204 | if not self._communication.connect(): 205 | messagebox.showerror('Error', "Can't connect device.") 206 | self._edit_on() 207 | return 208 | if 60 < _wait or 1023 < _size or _wait*1000 < _blank: 209 | messagebox.showerror('Error', 'Stop condition is invalid.') 210 | self._edit_on() 211 | return 212 | command = 'r[{},{},{}]\r\n'.format(_wait*1000, _blank, _size) 213 | ack = self._communication.record(command, _timeout) 214 | if ack[0]: 215 | self.signal_modified = True 216 | self.signal_override = ack[1] 217 | self._override_signal(self.signal_override) 218 | else: 219 | messagebox.showerror('Error', "Can't connect device.") 220 | self._edit_on() 221 | 222 | def _commit(self): 223 | """Operation when the commit button is pressed""" 224 | if self.signal_modified: 225 | self.signal_original = self.signal_override 226 | if self.comment_modified: 227 | self.comment_original = self._get_comment() 228 | self.enable(self.signal_original, self.comment_original) 229 | self.master.data_update(self.signal_original, self.comment_original) 230 | 231 | def _cancel(self): 232 | """Operation when the cancel button is pressed""" 233 | self.enable(self.signal_original, self.comment_original) 234 | 235 | def _send(self): 236 | """Operation when the send button is pressed""" 237 | self._edit_off() 238 | _timeout = 5 239 | if not self._communication.is_connect(): 240 | if not self._communication.connect(): 241 | messagebox.showerror('Error', "Can't connect device.") 242 | self._edit_on() 243 | return 244 | if self.signal_modified: 245 | if self.signal_override: 246 | command = 'w{}\r\n'.format(self.signal_override) 247 | else: 248 | command = '' 249 | else: 250 | if self.signal_original: 251 | command = 'w{}\r\n'.format(self.signal_original) 252 | else: 253 | command = '' 254 | if command: 255 | if not self._communication.send(command, _timeout): 256 | messagebox.showerror('Error', "Can't connect device.") 257 | self._edit_on() 258 | 259 | def _edit_off(self): 260 | self.record_button['state'] = Tk.DISABLED 261 | self.commit_button['state'] = Tk.DISABLED 262 | self.cancel_button['state'] = Tk.DISABLED 263 | self.send_button['state'] = Tk.DISABLED 264 | self.comment['state'] = Tk.DISABLED 265 | 266 | def _edit_on(self): 267 | self.record_button['state'] = Tk.NORMAL 268 | if self.signal_modified or self.comment_modified: 269 | self.commit_button['state'] = Tk.NORMAL 270 | self.cancel_button['state'] = Tk.NORMAL 271 | else: 272 | self.commit_button['state'] = Tk.DISABLED 273 | self.cancel_button['state'] = Tk.DISABLED 274 | self.send_button['state'] = Tk.NORMAL 275 | self.comment['state'] = Tk.NORMAL 276 | 277 | def _preset_signal(self, signal_list: list=[]): 278 | self.signal['state'] = Tk.NORMAL 279 | self.signal['bg'] = SignalFrame.COLOR_GREEN 280 | self.signal.delete('1.0', 'end') 281 | self.signal_original = signal_list 282 | self.signal_modified = False 283 | signal_text = '' 284 | for i,content in enumerate(self.signal_original): 285 | signal_text += '{:0>4d}: {:>7,d}\n'.format(i+1, content) 286 | self.signal.insert('1.0', signal_text) 287 | self.signal['state'] = Tk.DISABLED 288 | 289 | def _override_signal(self, signal_list: list): 290 | self.signal['state'] = Tk.NORMAL 291 | self.signal['bg'] = SignalFrame.COLOR_PINK 292 | self.signal.delete('1.0', 'end') 293 | self.signal_override = signal_list.copy() 294 | self.signal_modified = True 295 | signal_text = '' 296 | for i,content in enumerate(self.signal_override): 297 | signal_text += '{:0>4d}: {:>7,d}\n'.format(i+1, content) 298 | self.signal.insert('1.0', signal_text) 299 | self.signal['state'] = Tk.DISABLED 300 | 301 | def _get_comment(self): 302 | return(self.comment.get('1.0', 'end-1c')) 303 | 304 | def disable(self): 305 | """Disable all widgets""" 306 | self.signal['state'] = Tk.NORMAL 307 | # 1-line, 0-column to end 308 | self.signal.delete('1.0', 'end') 309 | self.signal['bg'] = SignalFrame.COLOR_GREEN 310 | self.signal['state'] = Tk.DISABLED 311 | self.comment['state'] = Tk.NORMAL 312 | self.comment.delete('1.0', 'end') 313 | self.comment['bg'] = SignalFrame.COLOR_GREEN 314 | self.comment['state'] = Tk.DISABLED 315 | self.record_button['state'] = Tk.DISABLED 316 | self.commit_button['state'] = Tk.DISABLED 317 | self.cancel_button['state'] = Tk.DISABLED 318 | self.send_button['state'] = Tk.DISABLED 319 | 320 | def enable(self, signal_list: list, comment_text: str): 321 | """Enable all widgets & Initialize""" 322 | self.signal_modified = False 323 | self.comment_modified = False 324 | self._preset_signal(signal_list) 325 | self.comment_original = comment_text 326 | 327 | self.comment['state'] = Tk.NORMAL 328 | self.comment['bg'] = SignalFrame.COLOR_GREEN 329 | self.comment.delete('1.0', 'end') 330 | self.comment.insert('1.0', comment_text) 331 | 332 | self.record_button['state'] = Tk.NORMAL 333 | self.commit_button['state'] = Tk.DISABLED 334 | self.cancel_button['state'] = Tk.DISABLED 335 | self.send_button['state'] = Tk.NORMAL 336 | -------------------------------------------------------------------------------- /demo/RP2040/micropython/main.py: -------------------------------------------------------------------------------- 1 | from machine import Pin 2 | from micropython import const 3 | from gc import collect 4 | import json 5 | from UpyIrTx import UpyIrTx 6 | from UpyIrRx import UpyIrRx 7 | 8 | # RP2040 RX=Pin18, TX=Pin19 9 | _GROVE_PIN = {'ATOM': (32, 26), 10 | 'CORE2': (33, 32), 11 | 'BASIC': (22, 21), 12 | 'GRAY': (22, 21), 13 | 'FIRE': (22, 21), 14 | 'GO': (22, 21), 15 | 'Stick': (33, 32), 16 | 'Else': (18, 19)} 17 | _DEVICE = 'Else' 18 | _TX_IDLE_LEVEL = const(0) 19 | _TX_FREQ = const(38000) 20 | _TX_DUTY = const(30) 21 | _RX_IDLE_LEVEL = const(1) 22 | _RX_SIZE = const(1023) 23 | 24 | rx_pin = Pin(_GROVE_PIN[_DEVICE][0], Pin.IN) 25 | rx = UpyIrRx(rx_pin, _RX_SIZE, _RX_IDLE_LEVEL) 26 | 27 | tx_pin = Pin(_GROVE_PIN[_DEVICE][1], Pin.OUT) 28 | tx = UpyIrTx(0, tx_pin, _TX_FREQ, _TX_DUTY, _TX_IDLE_LEVEL) 29 | 30 | cmd = input() 31 | while cmd != 'q': 32 | if len(cmd) > 0: 33 | if cmd[0] == 'r': 34 | # ex. cmd: 'r[3000, 200, 1023] 35 | try: 36 | _wait, _blank, _size = json.loads(cmd[1:]) 37 | if rx.record(_wait, _blank, _size) == UpyIrRx.ERROR_NONE: 38 | print(rx.get_calibrate_list()) 39 | else: 40 | print('[]') 41 | except: 42 | print('[]') 43 | elif cmd[0] == 'w': 44 | # ex. cmd: 'w[420, 1260, 420, ...] 45 | try: 46 | if tx.send(json.loads(cmd[1:])): 47 | print('OK') 48 | else: 49 | print('NG') 50 | except: 51 | print('NG') 52 | else: 53 | print('NG') 54 | del cmd 55 | collect() 56 | cmd = input() 57 | -------------------------------------------------------------------------------- /image/M5Stack.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meloncookie/RemotePy/fa6db71550b8a74d009694d54a20c7156a83f2ac/image/M5Stack.jpg -------------------------------------------------------------------------------- /image/RX_Circuit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meloncookie/RemotePy/fa6db71550b8a74d009694d54a20c7156a83f2ac/image/RX_Circuit.png -------------------------------------------------------------------------------- /image/TX_Circuit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meloncookie/RemotePy/fa6db71550b8a74d009694d54a20c7156a83f2ac/image/TX_Circuit.png -------------------------------------------------------------------------------- /image/TxRx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meloncookie/RemotePy/fa6db71550b8a74d009694d54a20c7156a83f2ac/image/TxRx.jpg -------------------------------------------------------------------------------- /image/gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meloncookie/RemotePy/fa6db71550b8a74d009694d54a20c7156a83f2ac/image/gui.png -------------------------------------------------------------------------------- /micropython/ESP32/FromV1_17/UpyIrRx.py: -------------------------------------------------------------------------------- 1 | from machine import Pin, disable_irq, enable_irq 2 | from micropython import const 3 | import time 4 | 5 | # IR RX class for ESP32 & RaspberryPi pico 6 | 7 | class UpyIrRx(): 8 | # Default record stop condition 9 | WAIT_MS_DEFAULT = const(5000) # [ms] 10 | BLANK_MS_DEFAULT = const(200) # [ms] 11 | MAX_DEFAULT = const(1023) 12 | # Binary bytes per sample 13 | UNIT_BYTES = const(3) 14 | 15 | # Record mode 16 | MODE_STAND_BY = const(0) # stop recording 17 | MODE_DONE_OK = const(1) 18 | MODE_DONE_NG = const(2) 19 | MODE_READY = const(3) # run recording 20 | MODE_RECORDING = const(4) 21 | 22 | # Error code 23 | ERROR_NONE = const(0) 24 | ERROR_NO_DATA = const(1) 25 | ERROR_OVERFLOW = const(2) 26 | ERROR_START_POINT = const(3) 27 | ERROR_END_POINT = const(4) 28 | ERROR_TIMEOUT = const(5) 29 | 30 | def __init__(self, pin, max_size=0, idle_level=1): 31 | self._pin = pin 32 | if max_size <= 0: 33 | self._max_size = UpyIrRx.MAX_DEFAULT 34 | else: 35 | if max_size % 2 == 0: 36 | self._max_size = max_size + 1 37 | else: 38 | self._max_size = max_size 39 | if idle_level: 40 | self._idle_level = 1 41 | else: 42 | self._idle_level = 0 43 | self._buffer = bytearray(self._max_size * UpyIrRx.UNIT_BYTES) 44 | self._record_size = 0 45 | self._mode = UpyIrRx.MODE_STAND_BY 46 | self._error = UpyIrRx.ERROR_NONE 47 | self._now = 0 48 | self._last = 0 49 | self._stop_size = 0 50 | dmy = self._pin.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self._callback) 51 | 52 | def get_mode(self): 53 | return(self._mode) 54 | 55 | def get_error_code(self): 56 | return(self._error) 57 | 58 | def get_record_buffer(self): 59 | if self._mode == UpyIrRx.MODE_DONE_OK: 60 | return(self._buffer) 61 | else: 62 | return(b'') 63 | 64 | def get_record_size(self): 65 | if self._mode == UpyIrRx.MODE_DONE_OK: 66 | return(self._record_size) 67 | else: 68 | return(0) 69 | 70 | def get_encode_bytes(self): 71 | return(UpyIrRx.UNIT_BYTES) 72 | 73 | def get_record_list(self): 74 | if self._mode == UpyIrRx.MODE_DONE_OK: 75 | return([int.from_bytes(self._buffer[i*UpyIrRx.UNIT_BYTES: (i+1)*UpyIrRx.UNIT_BYTES], 'little') for i in range(self._record_size)]) 76 | else: 77 | return([]) 78 | 79 | def get_calibrate_list(self): 80 | top32 = [9999]*32 81 | for i in range(self._record_size if self._record_size < 32 else 32): 82 | top32[i] = int.from_bytes(self._buffer[i*UpyIrRx.UNIT_BYTES: (i+1)*UpyIrRx.UNIT_BYTES], 'little') 83 | min_interval = min(top32) 84 | for i in range(31): 85 | if top32[i] < min_interval*1.4 and top32[i+1] < min_interval*1.4: 86 | basic_time = (top32[i]+top32[i+1]) // 2 87 | break 88 | else: 89 | return([]) 90 | return([round(int.from_bytes(self._buffer[i*UpyIrRx.UNIT_BYTES: (i+1)*UpyIrRx.UNIT_BYTES], 'little')/basic_time)*basic_time for i in range(self._record_size)]) 91 | 92 | def record(self, wait_ms=0, blank_ms=0, stop_size=0): 93 | if wait_ms <= 0: 94 | _wait_ms = UpyIrRx.WAIT_MS_DEFAULT 95 | else: 96 | _wait_ms = wait_ms 97 | if blank_ms <= 0: 98 | _blank_us = UpyIrRx.BLANK_MS_DEFAULT*1000 99 | else: 100 | _blank_us = blank_ms*1000 101 | if stop_size <= 0: 102 | self._stop_size = self._max_size 103 | else: 104 | if stop_size % 2 == 0: 105 | self._stop_size = stop_size + 1 106 | else: 107 | self._stop_size = stop_size 108 | if self._stop_size > self._max_size: 109 | self._stop_size = self._max_size 110 | self._record_size = 0 111 | self._error = UpyIrRx.ERROR_NONE 112 | if self._pin.value() != self._idle_level: 113 | self._mode = UpyIrRx.MODE_DONE_NG 114 | self._error = UpyIrRx.ERROR_START_POINT 115 | self._record_size = 0 116 | return(self._error) 117 | # begin recording 118 | self._mode = UpyIrRx.MODE_READY 119 | _start_us = time.ticks_us() 120 | time.sleep_ms(_wait_ms) 121 | # judgement 122 | if self._mode == UpyIrRx.MODE_DONE_NG: 123 | return(self._error) 124 | elif self._mode == UpyIrRx.MODE_DONE_OK: 125 | return(self._error) 126 | # begin critical 127 | irq_state = disable_irq() 128 | if self._mode == UpyIrRx.MODE_READY: 129 | self._mode = UpyIrRx.MODE_DONE_NG 130 | self._error = UpyIrRx.ERROR_NO_DATA 131 | self._record_size = 0 132 | elif time.ticks_diff(self._last, _start_us) + _blank_us > _wait_ms*1000: 133 | # < self._mode == UpyIrRx.MODE_RECORDING > 134 | self._mode = UpyIrRx.MODE_DONE_NG 135 | self._error = UpyIrRx.ERROR_TIMEOUT 136 | self._record_size = 0 137 | else: 138 | for i in range(self._record_size): 139 | if int.from_bytes(self._buffer[i*UpyIrRx.UNIT_BYTES: (i+1)*UpyIrRx.UNIT_BYTES], 'little') >= _blank_us: 140 | self._record_size = i 141 | break 142 | if self._record_size % 2 == 0: 143 | self._mode = UpyIrRx.MODE_DONE_NG 144 | self._error = UpyIrRx.ERROR_END_POINT 145 | self._record_size = 0 146 | else: 147 | self._mode = UpyIrRx.MODE_DONE_OK 148 | self._error = UpyIrRx.ERROR_NONE 149 | enable_irq(irq_state) 150 | # end critial 151 | return(self._error) 152 | 153 | def _callback(self, p): 154 | if self._mode == UpyIrRx.MODE_READY: 155 | self._last = time.ticks_us() 156 | self._mode = UpyIrRx.MODE_RECORDING 157 | elif self._mode == UpyIrRx.MODE_RECORDING: 158 | self._now = time.ticks_us() 159 | if self._record_size >= self._max_size: 160 | self._mode = UpyIrRx.MODE_DONE_NG 161 | self._error = UpyIrRx.ERROR_OVERFLOW 162 | self._record_size = 0 163 | return 164 | self._buffer[self._record_size*UpyIrRx.UNIT_BYTES: (self._record_size+1)*UpyIrRx.UNIT_BYTES] = time.ticks_diff(self._now, self._last).to_bytes(UpyIrRx.UNIT_BYTES, 'little') 165 | self._last = self._now 166 | self._record_size += 1 167 | if self._record_size >= self._stop_size: 168 | self._mode = UpyIrRx.MODE_DONE_OK 169 | self._error = UpyIrRx.ERROR_NONE 170 | -------------------------------------------------------------------------------- /micropython/ESP32/FromV1_17/UpyIrTx.py: -------------------------------------------------------------------------------- 1 | import esp32 2 | import time 3 | 4 | # IR TX class for ESP32 5 | # micropython v1.17 - v1.18(latest as of 2022/5) 6 | 7 | class UpyIrTx(): 8 | 9 | def __init__(self, ch, pin, freq=38000, duty=30, idle_level=0): 10 | self._raise = False 11 | if freq <= 0 or duty <= 0 or duty >= 100 or ch < 0 or ch > 7: 12 | raise(IndexError()) 13 | if idle_level: 14 | self._rmt = esp32.RMT(ch, pin=pin, clock_div=80, tx_carrier=(freq, (100-duty), 0), idle_level=True) 15 | self._posi = 0 16 | else: 17 | self._rmt = esp32.RMT(ch, pin=pin, clock_div=80, tx_carrier=(freq, duty, 1), idle_level=False) 18 | self._posi = 1 19 | 20 | def send_raw(self, signal_tuple): 21 | # Blocking until transmission 22 | # Value[us] must be less than 32,768(15bit) 23 | if signal_tuple: 24 | self._rmt.write_pulses(signal_tuple, self._posi) 25 | self._rmt.wait_done(timeout=2000) 26 | return(True) 27 | 28 | def send(self, signal_tuple): 29 | # Blocking until transmission 30 | # Value[us] is free 31 | if not signal_tuple: 32 | return(True) 33 | overindex = [] 34 | offsets = [] 35 | cumsum = 0 36 | len_signal = len(signal_tuple) 37 | if len_signal % 2 == 0: 38 | return(False) 39 | for i in range(len_signal): 40 | if signal_tuple[i] >= 32768: 41 | if i % 2 == 0: 42 | return(False) 43 | else: 44 | overindex.append(i) 45 | offsets.append(cumsum) 46 | cumsum = 0 47 | else: 48 | cumsum += signal_tuple[i] 49 | if len(overindex) == 0: 50 | self._rmt.write_pulses(signal_tuple, self._posi) 51 | self._rmt.wait_done(timeout=2000) 52 | else: 53 | last_index = 0 54 | for i in range(len(overindex)): 55 | self._rmt.write_pulses(signal_tuple[last_index: overindex[i]], self._posi) 56 | time.sleep_us(signal_tuple[overindex[i]]+offsets[i]) 57 | last_index = overindex[i] + 1 58 | self._rmt.write_pulses(signal_tuple[last_index: len_signal], self._posi) 59 | self._rmt.wait_done(timeout=2000) 60 | return(True) 61 | 62 | def send_cls(self, ir_rx): 63 | # Blocking until transmission 64 | if ir_rx.get_record_size() != 0: 65 | return(self.send(ir_rx.get_calibrate_list())) 66 | else: 67 | return(False) 68 | -------------------------------------------------------------------------------- /micropython/RP2040/FromV1_17/UpyIrRx.py: -------------------------------------------------------------------------------- 1 | from machine import Pin, disable_irq, enable_irq 2 | from micropython import const 3 | import time 4 | 5 | # IR RX class for ESP32 & RaspberryPi pico 6 | 7 | class UpyIrRx(): 8 | # Default record stop condition 9 | WAIT_MS_DEFAULT = const(5000) # [ms] 10 | BLANK_MS_DEFAULT = const(200) # [ms] 11 | MAX_DEFAULT = const(1023) 12 | # Binary bytes per sample 13 | UNIT_BYTES = const(3) 14 | 15 | # Record mode 16 | MODE_STAND_BY = const(0) # stop recording 17 | MODE_DONE_OK = const(1) 18 | MODE_DONE_NG = const(2) 19 | MODE_READY = const(3) # run recording 20 | MODE_RECORDING = const(4) 21 | 22 | # Error code 23 | ERROR_NONE = const(0) 24 | ERROR_NO_DATA = const(1) 25 | ERROR_OVERFLOW = const(2) 26 | ERROR_START_POINT = const(3) 27 | ERROR_END_POINT = const(4) 28 | ERROR_TIMEOUT = const(5) 29 | 30 | def __init__(self, pin, max_size=0, idle_level=1): 31 | self._pin = pin 32 | if max_size <= 0: 33 | self._max_size = UpyIrRx.MAX_DEFAULT 34 | else: 35 | if max_size % 2 == 0: 36 | self._max_size = max_size + 1 37 | else: 38 | self._max_size = max_size 39 | if idle_level: 40 | self._idle_level = 1 41 | else: 42 | self._idle_level = 0 43 | self._buffer = bytearray(self._max_size * UpyIrRx.UNIT_BYTES) 44 | self._record_size = 0 45 | self._mode = UpyIrRx.MODE_STAND_BY 46 | self._error = UpyIrRx.ERROR_NONE 47 | self._now = 0 48 | self._last = 0 49 | self._stop_size = 0 50 | dmy = self._pin.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self._callback) 51 | 52 | def get_mode(self): 53 | return(self._mode) 54 | 55 | def get_error_code(self): 56 | return(self._error) 57 | 58 | def get_record_buffer(self): 59 | if self._mode == UpyIrRx.MODE_DONE_OK: 60 | return(self._buffer) 61 | else: 62 | return(b'') 63 | 64 | def get_record_size(self): 65 | if self._mode == UpyIrRx.MODE_DONE_OK: 66 | return(self._record_size) 67 | else: 68 | return(0) 69 | 70 | def get_encode_bytes(self): 71 | return(UpyIrRx.UNIT_BYTES) 72 | 73 | def get_record_list(self): 74 | if self._mode == UpyIrRx.MODE_DONE_OK: 75 | return([int.from_bytes(self._buffer[i*UpyIrRx.UNIT_BYTES: (i+1)*UpyIrRx.UNIT_BYTES], 'little') for i in range(self._record_size)]) 76 | else: 77 | return([]) 78 | 79 | def get_calibrate_list(self): 80 | top32 = [9999]*32 81 | for i in range(self._record_size if self._record_size < 32 else 32): 82 | top32[i] = int.from_bytes(self._buffer[i*UpyIrRx.UNIT_BYTES: (i+1)*UpyIrRx.UNIT_BYTES], 'little') 83 | min_interval = min(top32) 84 | for i in range(31): 85 | if top32[i] < min_interval*1.4 and top32[i+1] < min_interval*1.4: 86 | basic_time = (top32[i]+top32[i+1]) // 2 87 | break 88 | else: 89 | return([]) 90 | return([round(int.from_bytes(self._buffer[i*UpyIrRx.UNIT_BYTES: (i+1)*UpyIrRx.UNIT_BYTES], 'little')/basic_time)*basic_time for i in range(self._record_size)]) 91 | 92 | def record(self, wait_ms=0, blank_ms=0, stop_size=0): 93 | if wait_ms <= 0: 94 | _wait_ms = UpyIrRx.WAIT_MS_DEFAULT 95 | else: 96 | _wait_ms = wait_ms 97 | if blank_ms <= 0: 98 | _blank_us = UpyIrRx.BLANK_MS_DEFAULT*1000 99 | else: 100 | _blank_us = blank_ms*1000 101 | if stop_size <= 0: 102 | self._stop_size = self._max_size 103 | else: 104 | if stop_size % 2 == 0: 105 | self._stop_size = stop_size + 1 106 | else: 107 | self._stop_size = stop_size 108 | if self._stop_size > self._max_size: 109 | self._stop_size = self._max_size 110 | self._record_size = 0 111 | self._error = UpyIrRx.ERROR_NONE 112 | if self._pin.value() != self._idle_level: 113 | self._mode = UpyIrRx.MODE_DONE_NG 114 | self._error = UpyIrRx.ERROR_START_POINT 115 | self._record_size = 0 116 | return(self._error) 117 | # begin recording 118 | self._mode = UpyIrRx.MODE_READY 119 | _start_us = time.ticks_us() 120 | time.sleep_ms(_wait_ms) 121 | # judgement 122 | if self._mode == UpyIrRx.MODE_DONE_NG: 123 | return(self._error) 124 | elif self._mode == UpyIrRx.MODE_DONE_OK: 125 | return(self._error) 126 | # begin critical 127 | irq_state = disable_irq() 128 | if self._mode == UpyIrRx.MODE_READY: 129 | self._mode = UpyIrRx.MODE_DONE_NG 130 | self._error = UpyIrRx.ERROR_NO_DATA 131 | self._record_size = 0 132 | elif time.ticks_diff(self._last, _start_us) + _blank_us > _wait_ms*1000: 133 | # < self._mode == UpyIrRx.MODE_RECORDING > 134 | self._mode = UpyIrRx.MODE_DONE_NG 135 | self._error = UpyIrRx.ERROR_TIMEOUT 136 | self._record_size = 0 137 | else: 138 | for i in range(self._record_size): 139 | if int.from_bytes(self._buffer[i*UpyIrRx.UNIT_BYTES: (i+1)*UpyIrRx.UNIT_BYTES], 'little') >= _blank_us: 140 | self._record_size = i 141 | break 142 | if self._record_size % 2 == 0: 143 | self._mode = UpyIrRx.MODE_DONE_NG 144 | self._error = UpyIrRx.ERROR_END_POINT 145 | self._record_size = 0 146 | else: 147 | self._mode = UpyIrRx.MODE_DONE_OK 148 | self._error = UpyIrRx.ERROR_NONE 149 | enable_irq(irq_state) 150 | # end critial 151 | return(self._error) 152 | 153 | def _callback(self, p): 154 | if self._mode == UpyIrRx.MODE_READY: 155 | self._last = time.ticks_us() 156 | self._mode = UpyIrRx.MODE_RECORDING 157 | elif self._mode == UpyIrRx.MODE_RECORDING: 158 | self._now = time.ticks_us() 159 | if self._record_size >= self._max_size: 160 | self._mode = UpyIrRx.MODE_DONE_NG 161 | self._error = UpyIrRx.ERROR_OVERFLOW 162 | self._record_size = 0 163 | return 164 | self._buffer[self._record_size*UpyIrRx.UNIT_BYTES: (self._record_size+1)*UpyIrRx.UNIT_BYTES] = time.ticks_diff(self._now, self._last).to_bytes(UpyIrRx.UNIT_BYTES, 'little') 165 | self._last = self._now 166 | self._record_size += 1 167 | if self._record_size >= self._stop_size: 168 | self._mode = UpyIrRx.MODE_DONE_OK 169 | self._error = UpyIrRx.ERROR_NONE 170 | -------------------------------------------------------------------------------- /micropython/RP2040/FromV1_17/UpyIrTx.py: -------------------------------------------------------------------------------- 1 | from rp2 import PIO, asm_pio, StateMachine 2 | from micropython import const 3 | 4 | # IR TX class for RaspberryPi pico 5 | # micropython v1.17 - v1.18(latest as of 2022/5) 6 | 7 | @asm_pio(autopull=True, pull_thresh=32, sideset_init=PIO.OUT_LOW) 8 | def pio_wave(): 9 | T = const(26) # Period: 1/38kHz*1M [us] 10 | OF_TIM = const(18) # Duty(30%) off time [us] 11 | OF_POR = const(0) # Idle level 12 | ON_TIM = const(8) # Duty(30%) on time [us] 13 | ON_POR = const(1) # not Idle level 14 | wrap_target() 15 | out(x, 32).side(OF_POR) 16 | label('on') 17 | set(y, ON_TIM).side(ON_POR) 18 | label('on_loop') 19 | jmp(x_dec, 'mid1').side(ON_POR) 20 | label('mid1') 21 | jmp(not_x, 'of').side(ON_POR) 22 | jmp(y_dec, 'on_loop').side(ON_POR) 23 | set(y, OF_TIM).side(OF_POR) 24 | label('of_loop') 25 | jmp(x_dec, 'mid2').side(OF_POR) 26 | label('mid2') 27 | jmp(not_x, 'of').side(OF_POR) 28 | jmp(y_dec, 'of_loop').side(OF_POR) 29 | jmp('on').side(OF_POR) 30 | label('of') 31 | out(x, 32).side(OF_POR) 32 | label('stay') 33 | jmp(x_dec, 'stay').side(OF_POR)[2] 34 | wrap() 35 | 36 | class UpyIrTx(): 37 | 38 | # Fixed: (freq=38000, duty=30, idle_level=0) 39 | def __init__(self, ch, pin, *args, **kwargs): 40 | self._sm = None 41 | if ch < 0 or ch > 7: 42 | raise(IndexError()) 43 | self._sm = StateMachine(ch, pio_wave, freq=3000000, sideset_base=pin) 44 | self._sm.active(1) 45 | 46 | def __del__(self): 47 | if self._sm: 48 | self._sm.active(0) 49 | 50 | def send(self, signal_tuple): 51 | # Blocking until transmission 52 | if not signal_tuple: 53 | return(True) 54 | if len(signal_tuple) % 2 == 0: 55 | return(False) 56 | for i in signal_tuple: 57 | self._sm.put(i) 58 | self._sm.put(100) # Last idle level 100us 59 | return(True) 60 | 61 | def send_cls(self, ir_rx): 62 | # Blocking until transmission 63 | if ir_rx.get_record_size() != 0: 64 | return(self.send(ir_rx.get_calibrate_list())) 65 | else: 66 | return(False) 67 | --------------------------------------------------------------------------------