From b19f454bf999c411fffce3efa93c29fddf7d78d8 Mon Sep 17 00:00:00 2001 From: IceDragon Date: Wed, 14 Jan 2026 19:18:40 +0200 Subject: [PATCH] Initial commit --- .gitignore | 16 +++ main.py | 134 +++++++++++++++++ requirements.txt | 1 + roland.py | 272 +++++++++++++++++++++++++++++++++++ src/__init__.py | 0 src/midi_controller.py | 247 +++++++++++++++++++++++++++++++ test/__init__.py | 0 test/test_midi_controller.py | 30 ++++ xplane.py | 61 ++++++++ 9 files changed, 761 insertions(+) create mode 100644 .gitignore create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 roland.py create mode 100644 src/__init__.py create mode 100644 src/midi_controller.py create mode 100644 test/__init__.py create mode 100644 test/test_midi_controller.py create mode 100644 xplane.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..19e7a1a --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Ignore JetBrains project files +.idea/ + +# Ignore Python cache directories +__pycache__/ +**/__pycache__/ + +# Ignore compiled Python files +*.pyc +*.pyo +*.pyd + +# Ignore macOS system files +.DS_Store +**/.DS_Store + diff --git a/main.py b/main.py new file mode 100644 index 0000000..a04a8f5 --- /dev/null +++ b/main.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3.11 +import itertools +import sys +import time +from typing import Optional, Tuple + +import rtmidi + +from xplane import XPlaneInput + + +G_STATUS_NAMES = { + 0x80: "NOTE_OFF", + 0x90: "NOTE_ON", + 0xa0: "KEY_PRESSR", + 0xb0: "CTRL_CHANGE", + 0xc0: "PROG_CHANGE", + 0xd0: "CHAN_PRESSR", + 0xe0: "PITCH_BEND", + 0xf0: "XXX", +} + + +def note_on(chan: int, note: int, velocity: int) -> Tuple[int, int, int]: + assert 0 < chan <= 16, chan + assert 0 <= note <= 0xFF, note + assert 0 <= velocity <= 0xFF, velocity + msg_byte = 0x90 + (chan - 1) + return msg_byte, note, velocity + + +def note_off(chan: int, note: int) -> Tuple[int, int, int]: + assert 0 < chan <= 16, chan + assert 0 <= note <= 0xFF, note + msg_byte = 0x80 + (chan - 1) + return msg_byte, note, 0 + + +def get_status_name(status: int) -> str: + return G_STATUS_NAMES.get(status & 0xF0) or f'' + + +def test_midi_out() -> int: + midi_out = rtmidi.MidiOut() + ports: list[str] = midi_out.get_ports() + print('Ports:') + for i, port_name in enumerate(ports): + print(f'{i:2d} {port_name}') + + # nanokey_index: int = ([i for i, port_name in enumerate(ports) if 'nanoKEY' in port_name] or [-1])[0] + # if nanokey_index < 0: + # print(f'nanoKEY2 port -> NOT FOUND [{ports}]', file=sys.stderr) + # return 1 + # + # print(f'nanoKEY2 port -> {nanokey_index} ({ports[nanokey_index]})') + # midi_out.open_port(nanokey_index) + midi_out.open_port(0) + with midi_out: + midi_out.send_message(note_on(1, 60, 112)) + time.sleep(0.5) + midi_out.send_message(note_off(1, 60)) + time.sleep(0.1) + + return 0 + + +def handle_midi_event(msg, midi_out): + midi_msg, delay = msg + status, b1, b2 = midi_msg + b_hi = b1 + (b2 << 8) + status_name = get_status_name(status) + # if status_name == "PITCH_BEND": + print(f'{status_name:11s} [ch {1 + (status & 0x0f)}] -> {b1:02x}[{b1:3d}] {b2:02x}[{b2:3d}] hi: {b_hi}') + # print(f'[d:{delay:2.2f}] {status_name:11s} [ch {1 + (status & 0x0f)}] -> {b1:02x}[{b1:3d}] {b2:02x}[{b2:3d}] hi: {b_hi}') + + midi_out.send_message([status, b1, 127]) + + +def set_all(midi_out, value): + for ch, btn_id in itertools.product([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 16], range(128)): + midi_out.send_message(note_on(ch, btn_id, value)) + + +def test_midi_in() -> int: + midi_in = rtmidi.MidiIn() + ports: list[str] = midi_in.get_ports() + print('Ports:') + for i, port_name in enumerate(ports): + print(f'{i:2d} {port_name}') + + midi_out = rtmidi.MidiOut() + + port = 1 + midi_in.set_callback(handle_midi_event, midi_out) + midi_in.open_port(port) + midi_out.open_port(port) + try: + set_all(midi_out, 127) + time.sleep(0.5) + set_all(midi_out, 0) + + i = 0 + levels = list(range(3)) + while True: + if i >= len(levels): + i = -len(levels) + 1 + + midi_out.send_message(note_on(3, 2, levels[abs(i)])) + + i += 1 + time.sleep(0.5) + except KeyboardInterrupt: + print('CTRL+C TRIGGERED') + finally: + set_all(midi_out, 0) + midi_in.cancel_callback() + midi_in.close_port() + + return 0 + + + + + + +def main() -> int: + return test_midi_in() + # xp = XPlaneInput('127.0.0.1', 49009) + # xp.run() + # return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0086a37 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +python-rtmidi diff --git a/roland.py b/roland.py new file mode 100644 index 0000000..f3d6e01 --- /dev/null +++ b/roland.py @@ -0,0 +1,272 @@ +from midi_controller import BasicControl, Button, ModeButton, Knob, Led, MultiLed, MidiMsg + + +class StepKnobButton(BasicControl): + STEP_CLOCKWISE = 1 + STEP_COUNTERCLOCKWISE = 127 + + EVENT_TYPE_STEP = MidiMsg.EVENT_TYPE_CTRL_CHANGE + EVENT_TYPE_BUTTON = Button.EVENT_TYPE + + def __init__(self, name: str, chan: int, ctrl_id: int, button: Button): + super().__init__(name, chan, ctrl_id) + self.button = button + + +class RolandDj202: + def __init__(self): + self.shiftButton = Button('SHIFT', chan=16, ctrl_id=0, led_class=Led) # both sides; LED might not work + + # middle section + self.songSelectorKnob = StepKnobButton( + 'SONG SELECTOR', + chan=16, + ctrl_id=0, + button=Button('SONG SELECTOR, BUTTON', chan=16, ctrl_id=6)) + + self.songSelectorKnobShift = StepKnobButton( + 'SONG SELECTOR (SHIFT)', + chan=16, + ctrl_id=1, + button=Button('SONG SELECTOR, BUTTON (SHIFT)', chan=16, ctrl_id=7)) + + self.mixingKnob = Knob('MIXING', chan=16, ctrl_id=13, mid_=64) + self.phonesLevelKnob = Knob('PHONES LEVEL', chan=16, ctrl_id=12) + self.samplerLevelKnob = Knob('SAMPLER LEVEL', chan=16, ctrl_id=26) + self.cueMiddleButton = Button('CUE (MIDDLE)', chan=16, ctrl_id=29, led_class=Led) + self.sequencerSyncButton = Button('SEQUENCER: SYNC', chan=16, ctrl_id=83, led_class=Led) + self.sequencerSyncButtonShift = Button('SEQUENCER: SYNC', chan=16, ctrl_id=85, led_class=Led) + # SEQUENCER: START/STOP / TAP -> not assigned yet + # SEQUENCER: START/STOP / TAP -> sequencer event not assigned yet + self.crossfaderKnob = Knob('CROSSFADER', chan=16, ctrl_id=8, mid_=64) + + # middle section (left and right), deck 1 and 2 + self.loadBpmButtonLeft = Button('LOAD/BPM, LEFT', chan=16, ctrl_id=2, led_class=Led) + self.loadBpmButtonLeftShift = Button('LOAD/BPM, LEFT', chan=16, ctrl_id=19, led_class=Led) + self.trimKnobLeft = Knob('TRIM, LEFT', chan=1, ctrl_id=22, ctrl_id2=52) + self.hiKnobLeft = Knob('HI, LEFT', chan=1, ctrl_id=23, ctrl_id2=53) + self.midKnobLeft = Knob('MID, LEFT', chan=1, ctrl_id=24, ctrl_id2=54) + self.lowKnobLeft = Knob('LOW, LEFT', chan=1, ctrl_id=25, ctrl_id2=55) + self.filterKnobLeft = Knob('FILTER, LEFT', chan=1, ctrl_id=26, ctrl_id2=None) + self.cueTapButtonLeft = Button('CUE/TAP, LEFT', chan=1, ctrl_id=27, led_class=Led) + self.cueTapButtonLeftShift = Button('CUE/TAP, LEFT, SHIFT', chan=1, ctrl_id=18, led_class=Led) + self.faderKnobLeft = Knob('FADER, LEFT', chan=1, ctrl_id=28, ctrl_id2=58) + + self.loadBpmButtonRight = Button('LOAD/BPM, RIGHT', chan=16, ctrl_id=3, led_class=Led) + self.loadBpmButtonRightShift = Button('LOAD/BPM, RIGHT', chan=16, ctrl_id=18, led_class=Led) + self.trimKnobRight = Knob('TRIM, RIGHT', chan=2, ctrl_id=22, ctrl_id2=52) + self.hiKnobRight = Knob('HI, RIGHT', chan=2, ctrl_id=23, ctrl_id2=53) + self.midKnobRight = Knob('MID, RIGHT', chan=2, ctrl_id=24, ctrl_id2=54) + self.lowKnobRight = Knob('LOW, RIGHT', chan=2, ctrl_id=25, ctrl_id2=55) + self.filterKnobRight = Knob('FILTER, RIGHT', chan=2, ctrl_id=26, ctrl_id2=None) + self.cueTapButtonRight = Button('CUE/TAP, RIGHT', chan=2, ctrl_id=27, led_class=Led) + self.cueTapButtonRightShift = Button('CUE/TAP, RIGHT, SHIFT', chan=2, ctrl_id=18, led_class=Led) + self.faderKnobRight = Knob('FADER, RIGHT', chan=2, ctrl_id=28, ctrl_id2=58) + + # fx section (left and right), deck 1 and 2 + self.fx1LevelBeatsKnobLeft = Knob('LEVEL/BEATS (FX 1), LEFT', chan=9, ctrl_id=0, ctrl_id2=None) + self.fx2LevelBeatsKnobLeft = Knob('LEVEL/BEATS (FX 2), LEFT', chan=9, ctrl_id=1, ctrl_id2=None) + self.fx3LevelBeatsKnobLeft = Knob('LEVEL/BEATS (FX 3), LEFT', chan=9, ctrl_id=2, ctrl_id2=None) + self.fx4LevelBeatsKnobLeft = Knob('LEVEL/BEATS (FX 4), LEFT, SHIFT', chan=9, ctrl_id=3, ctrl_id2=None) + self.fx1ButtonLeft = Button('FX 1, LEFT', chan=9, ctrl_id=0, led_class=Led) + self.fx2ButtonLeft = Button('FX 2, LEFT', chan=9, ctrl_id=1, led_class=Led) + self.fx3ButtonLeft = Button('FX 3, LEFT', chan=9, ctrl_id=2, led_class=Led) + self.tapFxModeButtonLeft = Button('TAP/FX MODE, LEFT', chan=9, ctrl_id=4, led_class=Led) + self.fx1ButtonLeftShift = Button('FX 1, LEFT, SHIFT', chan=9, ctrl_id=11, led_class=Led) + self.fx2ButtonLeftShift = Button('FX 2, LEFT, SHIFT', chan=9, ctrl_id=12, led_class=Led) + self.fx3ButtonLeftShift = Button('FX 3, LEFT, SHIFT', chan=9, ctrl_id=13, led_class=Led) + self.tapFxModeButtonLeftShift = Button('TAP/FX MODE, LEFT, SHIFT', chan=9, ctrl_id=10, led_class=Led) + + self.fx1LevelBeatsKnobRight = Knob('LEVEL/BEATS (FX 1), RIGHT', chan=10, ctrl_id=0, ctrl_id2=None) + self.fx2LevelBeatsKnobRight = Knob('LEVEL/BEATS (FX 2), RIGHT', chan=10, ctrl_id=1, ctrl_id2=None) + self.fx3LevelBeatsKnobRight = Knob('LEVEL/BEATS (FX 3), RIGHT', chan=10, ctrl_id=2, ctrl_id2=None) + self.fx4LevelBeatsKnobRight = Knob('LEVEL/BEATS (FX 4), RIGHT, SHIFT', chan=10, ctrl_id=3, ctrl_id2=None) + self.fx1ButtonRight = Button('FX 1, RIGHT', chan=10, ctrl_id=0, led_class=Led) + self.fx2ButtonRight = Button('FX 2, RIGHT', chan=10, ctrl_id=1, led_class=Led) + self.fx3ButtonRight = Button('FX 3, RIGHT', chan=10, ctrl_id=2, led_class=Led) + self.tapFxModeButtonRight = Button('TAP/FX MODE, RIGHT', chan=10, ctrl_id=4, led_class=Led) + self.fx1ButtonRightShift = Button('FX 1, RIGHT, SHIFT', chan=10, ctrl_id=11, led_class=Led) + self.fx2ButtonRightShift = Button('FX 2, RIGHT, SHIFT', chan=10, ctrl_id=12, led_class=Led) + self.fx3ButtonRightShift = Button('FX 3, RIGHT, SHIFT', chan=10, ctrl_id=13, led_class=Led) + self.tapFxModeButtonRightShift = Button('TAP/FX MODE, RIGHT, SHIFT', chan=10, ctrl_id=10, led_class=Led) + + # general deck section (left and right), deck 1 and 2 + self.tempoKnobLeft = Knob('TEMPO, LEFT', chan=1, ctrl_id=9, ctrl_id2=59, mid_=64) + self.vinylSlipButtonLeft = Button('VYNIL/SLIP, LEFT', chan=1, ctrl_id=7, led_class=Led) + # JOG WHEELS not yet assigned + self.syncButtonLeft = Button('SYNC, LEFT', chan=1, ctrl_id=2, led_class=Led) + self.syncButtonLeftShift = Button('SYNC, LEFT, SHIFT', chan=1, ctrl_id=3, led_class=Led) + self.cueButtonLeft = Button('CUE, LEFT', chan=1, ctrl_id=1, led_class=Led) + self.cueButtonLeftShift = Button('CUE, LEFT, SHIFT', chan=1, ctrl_id=5, led_class=Led) + self.playButtonLeft = Button('PLAY/PAUSE, LEFT', chan=1, ctrl_id=0, led_class=Led) + self.playButtonLeftShift = Button('PLAY/PAUSE, LEFT, SHIFT', chan=1, ctrl_id=4, led_class=Led) + self.keyLockRangeLeft = Button('KEY LOCK/RANGE, LEFT', chan=1, ctrl_id=13, led_class=Led) + self.keyLockRangeLeftShift = Button('KEY LOCK/RANGE, LEFT, SHIFT', chan=1, ctrl_id=14, led_class=Led) + + self.tempoKnobRight = Knob('TEMPO, RIGHT', chan=2, ctrl_id=9, ctrl_id2=59, mid_=64) + self.vinylSlipButtonRight = Button('VYNIL/SLIP, RIGHT', chan=2, ctrl_id=7, led_class=Led) + # JOG WHEELS not yet assigned + self.syncButtonRight = Button('SYNC, RIGHT', chan=2, ctrl_id=2, led_class=Led) + self.syncButtonRightShift = Button('SYNC, RIGHT, SHIFT', chan=2, ctrl_id=3, led_class=Led) + self.cueButtonRight = Button('CUE, RIGHT', chan=2, ctrl_id=1, led_class=Led) + self.cueButtonRightShift = Button('CUE, RIGHT, SHIFT', chan=2, ctrl_id=5, led_class=Led) + self.playButtonRight = Button('PLAY/PAUSE, RIGHT', chan=2, ctrl_id=0, led_class=Led) + self.playButtonRightShift = Button('PLAY/PAUSE, RIGHT, SHIFT', chan=2, ctrl_id=4, led_class=Led) + self.keyLockRangeRight = Button('KEY LOCK/RANGE, RIGHT', chan=2, ctrl_id=13, led_class=Led) + self.keyLockRangeRightShift = Button('KEY LOCK/RANGE, RIGHT, SHIFT', chan=2, ctrl_id=14, led_class=Led) + + # pads mode section (left and right), deck 1 and 2 + self.padModeHotCueLeft = ModeButton('PAD MODE: HOT CUE / CUE LOOP, LEFT', chan=5, ctrl_id=0, value=3) + self.padModeLoopRollLeft = ModeButton('PAD MODE: LOOP / ROLL, LEFT', chan=5, ctrl_id=0, value=19) + self.padModeSequencerPattern1Left = ModeButton('PAD MODE: SEQUENCER / PATTERN (1), LEFT', chan=5, ctrl_id=0, value=32) + self.padModeSequencerPattern2Left = ModeButton('PAD MODE: SEQUENCER / PATTERN (2), LEFT', chan=5, ctrl_id=0, value=34) + self.padModeSequencerPattern1LeftShift = ModeButton('PAD MODE: SEQUENCER / PATTERN (1), LEFT, SHIFT', chan=5, ctrl_id=0, value=33) + self.padModeSequencerPattern2LeftShift = ModeButton('PAD MODE: SEQUENCER / PATTERN (2), LEFT, SHIFT', chan=5, ctrl_id=0, value=35) + self.padModeSamplerSlicerLeft = ModeButton('PAD MODE: SAMPLER / SLICER, LEFT', chan=5, ctrl_id=0, value=51) + + self.padModeHotCueRight = ModeButton('PAD MODE: HOT CUE / CUE LOOP, RIGHT', chan=6, ctrl_id=0, value=3) + self.padModeLoopRollRight = ModeButton('PAD MODE: LOOP / ROLL, RIGHT', chan=6, ctrl_id=0, value=19) + self.padModeSequencerPattern1Right = ModeButton('PAD MODE: SEQUENCER / PATTERN (1), RIGHT', chan=6, ctrl_id=0, value=32) + self.padModeSequencerPattern2Right = ModeButton('PAD MODE: SEQUENCER / PATTERN (2), RIGHT', chan=6, ctrl_id=0, value=34) + self.padModeSequencerPattern1RightShift = ModeButton('PAD MODE: SEQUENCER / PATTERN (1), RIGHT, SHIFT', chan=6, ctrl_id=0, value=33) + self.padModeSequencerPattern2RightShift = ModeButton('PAD MODE: SEQUENCER / PATTERN (2), RIGHT, SHIFT', chan=6, ctrl_id=0, value=35) + self.padModeSamplerSlicerRight = ModeButton('PAD MODE: SAMPLER / SLICER, RIGHT', chan=6, ctrl_id=0, value=51) + + # pads mode 3 (HOT CUE / CUE LOOP) -> deck 1 and 2 + self.mode3ParamMinusButtonLeft = Button('MODE 3: PARAM MINUS, LEFT', chan=5, ctrl_id=67, led_class=Led) + self.mode3ParamPlusButtonLeft = Button('MODE 3: PARAM PLUS, LEFT', chan=5, ctrl_id=68, led_class=Led) + self.mode3Pad1ButtonLeft = Button('MODE 3: PAD 1, LEFT', chan=5, ctrl_id=1, led_class=MultiLed) + self.mode3Pad2ButtonLeft = Button('MODE 3: PAD 2, LEFT', chan=5, ctrl_id=2, led_class=MultiLed) + self.mode3Pad3ButtonLeft = Button('MODE 3: PAD 3, LEFT', chan=5, ctrl_id=3, led_class=MultiLed) + self.mode3Pad4ButtonLeft = Button('MODE 3: PAD 4, LEFT', chan=5, ctrl_id=4, led_class=MultiLed) + self.mode3Pad5ButtonLeft = Button('MODE 3: PAD 5, LEFT', chan=5, ctrl_id=5, led_class=MultiLed) + self.mode3Pad6ButtonLeft = Button('MODE 3: PAD 6, LEFT', chan=5, ctrl_id=6, led_class=MultiLed) + self.mode3Pad7ButtonLeft = Button('MODE 3: PAD 7, LEFT', chan=5, ctrl_id=7, led_class=MultiLed) + self.mode3Pad8ButtonLeft = Button('MODE 3: PAD 8, LEFT', chan=5, ctrl_id=8, led_class=MultiLed) + + self.mode3ParamMinusButtonLeftShift = Button('MODE 3: PARAM MINUS, LEFT, SHIFT', chan=5, ctrl_id=75, led_class=Led) + self.mode3ParamPlusButtonLeftShift = Button('MODE 3: PARAM PLUS, LEFT, SHIFT', chan=5, ctrl_id=76, led_class=Led) + self.mode3Pad1ButtonLeftShift = Button('MODE 3: PAD 1, LEFT, SHIFT', chan=5, ctrl_id=9, led_class=MultiLed) + self.mode3Pad2ButtonLeftShift = Button('MODE 3: PAD 2, LEFT, SHIFT', chan=5, ctrl_id=10, led_class=MultiLed) + self.mode3Pad3ButtonLeftShift = Button('MODE 3: PAD 3, LEFT, SHIFT', chan=5, ctrl_id=11, led_class=MultiLed) + self.mode3Pad4ButtonLeftShift = Button('MODE 3: PAD 4, LEFT, SHIFT', chan=5, ctrl_id=12, led_class=MultiLed) + self.mode3Pad5ButtonLeftShift = Button('MODE 3: PAD 5, LEFT, SHIFT', chan=5, ctrl_id=13, led_class=MultiLed) + self.mode3Pad6ButtonLeftShift = Button('MODE 3: PAD 6, LEFT, SHIFT', chan=5, ctrl_id=14, led_class=MultiLed) + self.mode3Pad7ButtonLeftShift = Button('MODE 3: PAD 7, LEFT, SHIFT', chan=5, ctrl_id=15, led_class=MultiLed) + self.mode3Pad8ButtonLeftShift = Button('MODE 3: PAD 8, LEFT, SHIFT', chan=5, ctrl_id=16, led_class=MultiLed) + + self.mode3ParamMinusButtonRight = Button('MODE 3: PARAM MINUS, RIGHT', chan=6, ctrl_id=67, led_class=Led) + self.mode3ParamPlusButtonRight = Button('MODE 3: PARAM PLUS, RIGHT', chan=6, ctrl_id=68, led_class=Led) + self.mode3Pad1ButtonRight = Button('MODE 3: PAD 1, RIGHT', chan=6, ctrl_id=1, led_class=MultiLed) + self.mode3Pad2ButtonRight = Button('MODE 3: PAD 2, RIGHT', chan=6, ctrl_id=2, led_class=MultiLed) + self.mode3Pad3ButtonRight = Button('MODE 3: PAD 3, RIGHT', chan=6, ctrl_id=3, led_class=MultiLed) + self.mode3Pad4ButtonRight = Button('MODE 3: PAD 4, RIGHT', chan=6, ctrl_id=4, led_class=MultiLed) + self.mode3Pad5ButtonRight = Button('MODE 3: PAD 5, RIGHT', chan=6, ctrl_id=5, led_class=MultiLed) + self.mode3Pad6ButtonRight = Button('MODE 3: PAD 6, RIGHT', chan=6, ctrl_id=6, led_class=MultiLed) + self.mode3Pad7ButtonRight = Button('MODE 3: PAD 7, RIGHT', chan=6, ctrl_id=7, led_class=MultiLed) + self.mode3Pad8ButtonRight = Button('MODE 3: PAD 8, RIGHT', chan=6, ctrl_id=8, led_class=MultiLed) + + self.mode3ParamMinusButtonRightShift = Button('MODE 3: PARAM MINUS, RIGHT, SHIFT', chan=6, ctrl_id=75, led_class=Led) + self.mode3ParamPlusButtonRightShift = Button('MODE 3: PARAM PLUS, RIGHT, SHIFT', chan=6, ctrl_id=76, led_class=Led) + self.mode3Pad1ButtonRightShift = Button('MODE 3: PAD 1, RIGHT, SHIFT', chan=6, ctrl_id=9, led_class=MultiLed) + self.mode3Pad2ButtonRightShift = Button('MODE 3: PAD 2, RIGHT, SHIFT', chan=6, ctrl_id=10, led_class=MultiLed) + self.mode3Pad3ButtonRightShift = Button('MODE 3: PAD 3, RIGHT, SHIFT', chan=6, ctrl_id=11, led_class=MultiLed) + self.mode3Pad4ButtonRightShift = Button('MODE 3: PAD 4, RIGHT, SHIFT', chan=6, ctrl_id=12, led_class=MultiLed) + self.mode3Pad5ButtonRightShift = Button('MODE 3: PAD 5, RIGHT, SHIFT', chan=6, ctrl_id=13, led_class=MultiLed) + self.mode3Pad6ButtonRightShift = Button('MODE 3: PAD 6, RIGHT, SHIFT', chan=6, ctrl_id=14, led_class=MultiLed) + self.mode3Pad7ButtonRightShift = Button('MODE 3: PAD 7, RIGHT, SHIFT', chan=6, ctrl_id=15, led_class=MultiLed) + self.mode3Pad8ButtonRightShift = Button('MODE 3: PAD 8, RIGHT, SHIFT', chan=6, ctrl_id=16, led_class=MultiLed) + + # pads mode 19 (LOOP / ROLL) -> deck 1 and 2 + self.mode19ParamMinusButtonLeft = Button('MODE 19: PARAM MINUS, LEFT', chan=5, ctrl_id=65, led_class=Led) + self.mode19ParamPlusButtonLeft = Button('MODE 19: PARAM PLUS, LEFT', chan=5, ctrl_id=66, led_class=Led) + self.mode19Pad1ButtonLeft = Button('MODE 19: PAD 1, LEFT', chan=5, ctrl_id=17, led_class=MultiLed) + self.mode19Pad2ButtonLeft = Button('MODE 19: PAD 2, LEFT', chan=5, ctrl_id=18, led_class=MultiLed) + self.mode19Pad3ButtonLeft = Button('MODE 19: PAD 3, LEFT', chan=5, ctrl_id=19, led_class=MultiLed) + self.mode19Pad4ButtonLeft = Button('MODE 19: PAD 4, LEFT', chan=5, ctrl_id=20, led_class=MultiLed) + self.mode19Pad5ButtonLeft = Button('MODE 19: PAD 5, LEFT', chan=5, ctrl_id=21, led_class=MultiLed) + self.mode19Pad6ButtonLeft = Button('MODE 19: PAD 6, LEFT', chan=5, ctrl_id=22, led_class=MultiLed) + self.mode19Pad7ButtonLeft = Button('MODE 19: PAD 7, LEFT', chan=5, ctrl_id=23, led_class=MultiLed) + self.mode19Pad8ButtonLeft = Button('MODE 19: PAD 8, LEFT', chan=5, ctrl_id=24, led_class=MultiLed) + + self.mode19ParamMinusButtonLeftShift = Button('MODE 19: PARAM MINUS, LEFT, SHIFT', chan=5, ctrl_id=73, led_class=Led) + self.mode19ParamPlusButtonLeftShift = Button('MODE 19: PARAM PLUS, LEFT, SHIFT', chan=5, ctrl_id=74, led_class=Led) + self.mode19Pad1ButtonLeftShift = Button('MODE 19: PAD 1, LEFT, SHIFT', chan=5, ctrl_id=25, led_class=MultiLed) + self.mode19Pad2ButtonLeftShift = Button('MODE 19: PAD 2, LEFT, SHIFT', chan=5, ctrl_id=26, led_class=MultiLed) + self.mode19Pad3ButtonLeftShift = Button('MODE 19: PAD 3, LEFT, SHIFT', chan=5, ctrl_id=27, led_class=MultiLed) + self.mode19Pad4ButtonLeftShift = Button('MODE 19: PAD 4, LEFT, SHIFT', chan=5, ctrl_id=28, led_class=MultiLed) + self.mode19Pad5ButtonLeftShift = Button('MODE 19: PAD 5, LEFT, SHIFT', chan=5, ctrl_id=29, led_class=MultiLed) + self.mode19Pad6ButtonLeftShift = Button('MODE 19: PAD 6, LEFT, SHIFT', chan=5, ctrl_id=30, led_class=MultiLed) + self.mode19Pad7ButtonLeftShift = Button('MODE 19: PAD 7, LEFT, SHIFT', chan=5, ctrl_id=31, led_class=MultiLed) + self.mode19Pad8ButtonLeftShift = Button('MODE 19: PAD 8, LEFT, SHIFT', chan=5, ctrl_id=32, led_class=MultiLed) + + self.mode19ParamMinusButtonRight = Button('MODE 19: PARAM MINUS, RIGHT', chan=6, ctrl_id=65, led_class=Led) + self.mode19ParamPlusButtonRight = Button('MODE 19: PARAM PLUS, RIGHT', chan=6, ctrl_id=66, led_class=Led) + self.mode19Pad1ButtonRight = Button('MODE 19: PAD 1, RIGHT', chan=6, ctrl_id=17, led_class=MultiLed) + self.mode19Pad2ButtonRight = Button('MODE 19: PAD 2, RIGHT', chan=6, ctrl_id=18, led_class=MultiLed) + self.mode19Pad3ButtonRight = Button('MODE 19: PAD 3, RIGHT', chan=6, ctrl_id=19, led_class=MultiLed) + self.mode19Pad4ButtonRight = Button('MODE 19: PAD 4, RIGHT', chan=6, ctrl_id=20, led_class=MultiLed) + self.mode19Pad5ButtonRight = Button('MODE 19: PAD 5, RIGHT', chan=6, ctrl_id=21, led_class=MultiLed) + self.mode19Pad6ButtonRight = Button('MODE 19: PAD 6, RIGHT', chan=6, ctrl_id=22, led_class=MultiLed) + self.mode19Pad7ButtonRight = Button('MODE 19: PAD 7, RIGHT', chan=6, ctrl_id=23, led_class=MultiLed) + self.mode19Pad8ButtonRight = Button('MODE 19: PAD 8, RIGHT', chan=6, ctrl_id=24, led_class=MultiLed) + + self.mode19ParamMinusButtonRightShift = Button('MODE 19: PARAM MINUS, RIGHT, SHIFT', chan=6, ctrl_id=73, led_class=Led) + self.mode19ParamPlusButtonRightShift = Button('MODE 19: PARAM PLUS, RIGHT, SHIFT', chan=6, ctrl_id=74, led_class=Led) + self.mode19Pad1ButtonRightShift = Button('MODE 19: PAD 1, RIGHT, SHIFT', chan=6, ctrl_id=25, led_class=MultiLed) + self.mode19Pad2ButtonRightShift = Button('MODE 19: PAD 2, RIGHT, SHIFT', chan=6, ctrl_id=26, led_class=MultiLed) + self.mode19Pad3ButtonRightShift = Button('MODE 19: PAD 3, RIGHT, SHIFT', chan=6, ctrl_id=27, led_class=MultiLed) + self.mode19Pad4ButtonRightShift = Button('MODE 19: PAD 4, RIGHT, SHIFT', chan=6, ctrl_id=28, led_class=MultiLed) + self.mode19Pad5ButtonRightShift = Button('MODE 19: PAD 5, RIGHT, SHIFT', chan=6, ctrl_id=29, led_class=MultiLed) + self.mode19Pad6ButtonRightShift = Button('MODE 19: PAD 6, RIGHT, SHIFT', chan=6, ctrl_id=30, led_class=MultiLed) + self.mode19Pad7ButtonRightShift = Button('MODE 19: PAD 7, RIGHT, SHIFT', chan=6, ctrl_id=31, led_class=MultiLed) + self.mode19Pad8ButtonRightShift = Button('MODE 19: PAD 8, RIGHT, SHIFT', chan=6, ctrl_id=32, led_class=MultiLed) + + # pads mode 51 (SAMPLER / SLICER) -> deck 1 and 2 + self.mode51ParamMinusButtonLeft = Button('MODE 51: PARAM MINUS, LEFT', chan=5, ctrl_id=69, led_class=Led) + self.mode51ParamPlusButtonLeft = Button('MODE 51: PARAM PLUS, LEFT', chan=5, ctrl_id=70, led_class=Led) + self.mode51Pad1ButtonLeft = Button('MODE 51: PAD 1, LEFT', chan=5, ctrl_id=33, led_class=MultiLed) + self.mode51Pad2ButtonLeft = Button('MODE 51: PAD 2, LEFT', chan=5, ctrl_id=34, led_class=MultiLed) + self.mode51Pad3ButtonLeft = Button('MODE 51: PAD 3, LEFT', chan=5, ctrl_id=35, led_class=MultiLed) + self.mode51Pad4ButtonLeft = Button('MODE 51: PAD 4, LEFT', chan=5, ctrl_id=36, led_class=MultiLed) + self.mode51Pad5ButtonLeft = Button('MODE 51: PAD 5, LEFT', chan=5, ctrl_id=37, led_class=MultiLed) + self.mode51Pad6ButtonLeft = Button('MODE 51: PAD 6, LEFT', chan=5, ctrl_id=38, led_class=MultiLed) + self.mode51Pad7ButtonLeft = Button('MODE 51: PAD 7, LEFT', chan=5, ctrl_id=39, led_class=MultiLed) + self.mode51Pad8ButtonLeft = Button('MODE 51: PAD 8, LEFT', chan=5, ctrl_id=40, led_class=MultiLed) + + self.mode51ParamMinusButtonLeftShift = Button('MODE 51: PARAM MINUS, LEFT, SHIFT', chan=5, ctrl_id=77, led_class=Led) + self.mode51ParamPlusButtonLeftShift = Button('MODE 51: PARAM PLUS, LEFT, SHIFT', chan=5, ctrl_id=78, led_class=Led) + self.mode51Pad1ButtonLeftShift = Button('MODE 51: PAD 1, LEFT, SHIFT', chan=5, ctrl_id=41, led_class=MultiLed) + self.mode51Pad2ButtonLeftShift = Button('MODE 51: PAD 2, LEFT, SHIFT', chan=5, ctrl_id=42, led_class=MultiLed) + self.mode51Pad3ButtonLeftShift = Button('MODE 51: PAD 3, LEFT, SHIFT', chan=5, ctrl_id=43, led_class=MultiLed) + self.mode51Pad4ButtonLeftShift = Button('MODE 51: PAD 4, LEFT, SHIFT', chan=5, ctrl_id=44, led_class=MultiLed) + self.mode51Pad5ButtonLeftShift = Button('MODE 51: PAD 5, LEFT, SHIFT', chan=5, ctrl_id=45, led_class=MultiLed) + self.mode51Pad6ButtonLeftShift = Button('MODE 51: PAD 6, LEFT, SHIFT', chan=5, ctrl_id=46, led_class=MultiLed) + self.mode51Pad7ButtonLeftShift = Button('MODE 51: PAD 7, LEFT, SHIFT', chan=5, ctrl_id=47, led_class=MultiLed) + self.mode51Pad8ButtonLeftShift = Button('MODE 51: PAD 8, LEFT, SHIFT', chan=5, ctrl_id=48, led_class=MultiLed) + + self.mode51ParamMinusButtonRight = Button('MODE 51: PARAM MINUS, RIGHT', chan=6, ctrl_id=69, led_class=Led) + self.mode51ParamPlusButtonRight = Button('MODE 51: PARAM PLUS, RIGHT', chan=6, ctrl_id=70, led_class=Led) + self.mode51Pad1ButtonRight = Button('MODE 51: PAD 1, RIGHT', chan=6, ctrl_id=33, led_class=MultiLed) + self.mode51Pad2ButtonRight = Button('MODE 51: PAD 2, RIGHT', chan=6, ctrl_id=34, led_class=MultiLed) + self.mode51Pad3ButtonRight = Button('MODE 51: PAD 3, RIGHT', chan=6, ctrl_id=35, led_class=MultiLed) + self.mode51Pad4ButtonRight = Button('MODE 51: PAD 4, RIGHT', chan=6, ctrl_id=36, led_class=MultiLed) + self.mode51Pad5ButtonRight = Button('MODE 51: PAD 5, RIGHT', chan=6, ctrl_id=37, led_class=MultiLed) + self.mode51Pad6ButtonRight = Button('MODE 51: PAD 6, RIGHT', chan=6, ctrl_id=38, led_class=MultiLed) + self.mode51Pad7ButtonRight = Button('MODE 51: PAD 7, RIGHT', chan=6, ctrl_id=39, led_class=MultiLed) + self.mode51Pad8ButtonRight = Button('MODE 51: PAD 8, RIGHT', chan=6, ctrl_id=40, led_class=MultiLed) + + self.mode51ParamMinusButtonRightShift = Button('MODE 51: PARAM MINUS, RIGHT, SHIFT', chan=6, ctrl_id=77, led_class=Led) + self.mode51ParamPlusButtonRightShift = Button('MODE 51: PARAM PLUS, RIGHT, SHIFT', chan=6, ctrl_id=78, led_class=Led) + self.mode51Pad1ButtonRightShift = Button('MODE 51: PAD 1, RIGHT, SHIFT', chan=6, ctrl_id=41, led_class=MultiLed) + self.mode51Pad2ButtonRightShift = Button('MODE 51: PAD 2, RIGHT, SHIFT', chan=6, ctrl_id=42, led_class=MultiLed) + self.mode51Pad3ButtonRightShift = Button('MODE 51: PAD 3, RIGHT, SHIFT', chan=6, ctrl_id=43, led_class=MultiLed) + self.mode51Pad4ButtonRightShift = Button('MODE 51: PAD 4, RIGHT, SHIFT', chan=6, ctrl_id=44, led_class=MultiLed) + self.mode51Pad5ButtonRightShift = Button('MODE 51: PAD 5, RIGHT, SHIFT', chan=6, ctrl_id=45, led_class=MultiLed) + self.mode51Pad6ButtonRightShift = Button('MODE 51: PAD 6, RIGHT, SHIFT', chan=6, ctrl_id=46, led_class=MultiLed) + self.mode51Pad7ButtonRightShift = Button('MODE 51: PAD 7, RIGHT, SHIFT', chan=6, ctrl_id=47, led_class=MultiLed) + self.mode51Pad8ButtonRightShift = Button('MODE 51: PAD 8, RIGHT, SHIFT', chan=6, ctrl_id=48, led_class=MultiLed) + + # deck switch buttons + self.deck3Button = Button('DECK 3', chan=1, ctrl_id=8, led_class=Led) + self.deck4Button = Button('DECK 4', chan=2, ctrl_id=8, led_class=Led) diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/midi_controller.py b/src/midi_controller.py new file mode 100644 index 0000000..6ebcd3a --- /dev/null +++ b/src/midi_controller.py @@ -0,0 +1,247 @@ +from array import array +from dataclasses import dataclass +from typing import Optional, List + + +MIDI_MSG_TYPES = { + 0x80: "NOTE_OFF", + 0x90: "NOTE_ON", + 0xa0: "KEY_PRESSURE", + 0xb0: "CTRL_CHANGE", + 0xc0: "PROG_CHANGE", + 0xd0: "CHAN_PRESSURE", + 0xe0: "PITCH_BEND", + 0xf0: "SYSTEM", +} + + +class MidiMsg: + """ + Represents a single MIDI message + """ + EVENT_TYPE_NOTE_OFF = 0x80 + EVENT_TYPE_NOTE_ON = 0x90 + EVENT_TYPE_KEY_PRESSURE = 0xa0 + EVENT_TYPE_CTRL_CHANGE = 0xb0 + EVENT_TYPE_PROG_CHANGE = 0xc0 + EVENT_TYPE_CHAN_PRESSURE = 0xd0 + EVENT_TYPE_PITCH_BEND = 0xe0 + EVENT_TYPE_SYSTEM = 0xf0 + + @classmethod + def create(cls, status_byte: int, data0: int, data1: int = 0) -> 'MidiMsg': + return cls(array("B", (status_byte, data0, data1))) + + @classmethod + def create_from_bytes(cls, byte_array: array) -> 'MidiMsg': + return cls(byte_array) + + def __init__(self, b: array): + self.__bytes = b + + def __str__(self) -> str: + return f'{self.msg_type_str}(chan={self.channel}, b0={self.data0}, b1={self.data1})' + + @property + def msg_type(self) -> int: + return self.__bytes[0] & 0xf0 + + @property + def msg_type_str(self) -> str: + return MIDI_MSG_TYPES.get(self.msg_type, "") + + @property + def channel(self) -> int: + return (self.__bytes[0] & 0x0f) + 1 + + @property + def data0(self) -> int: + return self.__bytes[1] + + @property + def data1(self) -> int: + return self.__bytes[2] + + def as_tuple(self) -> (int, int, int): + return tuple(self.__bytes) + + +class BasicControl: + MIN_STATE = 0x00 + MAX_STATE = 0x7f + DEFAULT_STATE = MIN_STATE + + def __init__(self, name: str, midi_channel: int, control_id: int, state: int = DEFAULT_STATE): + """ + :param name: name of this control + :param midi_channel: MIDI channel this control operates on + :param control_id: MIDI control (button/knob/led) id (first MIDI data byte) + :param state: initial state of this control + """ + assert 1 <= midi_channel <= 16, f'invalid MIDI channel {midi_channel} (expected 1..16)' + assert 0 <= control_id <= 127, f'invalid control id {control_id} (expected 0..127)' + assert 0 <= state <= 127, f'invalid state {state} (expected 0..127)' + self.name: str = name + self._channel: int = midi_channel + self._id: int = control_id + self._state: int = state + + def __str__(self) -> str: + """ + :return: name of this control + """ + return self.name + + ############################################################################################### + # PROPERTIES ################################################################################## + ############################################################################################### + + @property + def id(self) -> int: + """ + :return: control id + """ + return self._id + + @property + def channel(self) -> int: + """ + :return: MIDI channel this device operates on + """ + return self.channel + + @property + def state(self) -> int: + """ + :return: current state of this control + """ + return self._state + + ############################################################################################### + # MUTATION #################################################################################### + ############################################################################################### + + def change_state(self, state: int): + """Change/set current state of this control + + :param state: new state of this control + """ + if not (self.MIN_STATE <= state <= self.MAX_STATE): + raise ValueError(f'invalid state: {state} (expected {self.MIN_STATE}..{self.MAX_STATE})') + + self._state = state + # TODO: notify state changed + + +class Led(BasicControl): + LIGHT_OFF = 0x00 + LIGHT_ON = 0x7f + + def __init__(self, name: str, chan: int, ctrl_id: int, states: List[int] = (LIGHT_OFF, LIGHT_ON)): + """ + :param name: name of this LED control + :param chan: MIDI channel used for this LED + :param ctrl_id: MIDI control ID + :param states: all available states, from the dimmest to the brightest + """ + assert states, 'states cannot be empty' + super().__init__(name, chan, ctrl_id, state=states[0]) + + +class MultiLed(Led): + LIGHT_OFF = 0x00 + LIGHT_DIM = 0x01 + LIGHT_ON = 0x02 + + def __init__(self, name: str, chan: int, ctrl_id: int, states: List[int] = (LIGHT_OFF, LIGHT_DIM, LIGHT_ON)): + """ + :param name: name of this LED control + :param chan: MIDI channel used for this LED + :param ctrl_id: MIDI control ID + :param states: all available states, from the dimmest to the brightest + """ + assert states, 'states cannot be empty' + super().__init__(name, chan, ctrl_id, states) + + +class Button(BasicControl): + STATE_RELEASED = BasicControl.MIN_STATE + STATE_PRESSED = BasicControl.MAX_STATE + + EVENT_TYPE = MidiMsg.EVENT_TYPE_NOTE_ON + + def __init__(self, name: str, chan: int, ctrl_id: int, led_class: Optional[type] = None): + """ + :param name: name of this button + :param chan: MIDI channel used for this button + :param ctrl_id: button ID (i.e., MIDI data byte 1) + :param led_class: (optional) LED control associated with this button + """ + super().__init__(name, chan, ctrl_id, state=self.STATE_RELEASED) + self._led: Optional[Led] = led_class and led_class(f'LED: {name}', chan, ctrl_id) + + ############################################################################################### + # PROPERTIES ################################################################################## + ############################################################################################### + + @property + def led(self) -> Optional[Led]: + return self._led + + @property + def has_led(self) -> bool: + """ + :return: True if this button has an LED + """ + return self._led is not None + + @property + def is_pressed(self): + """ + :return: True if this button is currently pressed + """ + return self.state == self.STATE_PRESSED + + +class ModeButton(BasicControl): + EVENT_TYPE = MidiMsg.EVENT_TYPE_NOTE_ON + + def __init__(self, name: str, chan: int, ctrl_id: int, value: int): + """ + :param name: name of this button + :param chan: MIDI channel used for this button + :param ctrl_id: button ID (i.e., MIDI data byte 1) + :param value: button mode (i.e., MIDI data byte 2) + """ + super().__init__(name, chan, ctrl_id) + self.value = value + + +class Knob(BasicControl): + STATE_MAX = 0x7f + STATE_MIN = 0x00 + + EVENT_TYPE = MidiMsg.EVENT_TYPE_CTRL_CHANGE + + def __init__(self, name: str, chan: int, ctrl_id: int, ctrl_id2: int = None, min_: int = STATE_MIN, max_: int = STATE_MAX, mid_: Optional[int] = None): + """ + :param name: name of the knob + :param chan: MIDI channel + :param ctrl_id: control id + :param ctrl_id2: secondary control id for precision marks (0, 64, 112) + :param min_: minimal value of the knob (usually 0) + :param max_: maximal value of the knob (usually 127, or 0x7f) + :param mid_: midpoint val of the knob, or None if no specific midpoint + """ + super().__init__(name, chan, ctrl_id, state=min_) + self.precision_ctrl_id = ctrl_id2 # TODO: actually do something with this control + self.min = min_ + self.max = max_ + self.mid = mid_ + + @property + def has_midpoint(self) -> bool: + """ + :return: True if this knob has a concrete midpoint; False if midpoint is somewhere between min and max + """ + return self.mid is not None diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/test_midi_controller.py b/test/test_midi_controller.py new file mode 100644 index 0000000..af65457 --- /dev/null +++ b/test/test_midi_controller.py @@ -0,0 +1,30 @@ +from array import array +from unittest import TestCase + +from midi_controller import MidiMsg + + +class TestMidiMsg(TestCase): + def test_create(self): + msg = MidiMsg.create(0x92, 127, 127) + self.assertEqual(0x90, msg.msg_type) + self.assertEqual('NOTE_ON', msg.msg_type_str) + self.assertEqual(3, msg.channel) + self.assertEqual(127, msg.data0) + self.assertEqual(127, msg.data1) + self.assertEqual((0x92, 0x7f, 0x7f), msg.as_tuple()) + + def test_create_from_bytes(self): + msg = MidiMsg.create_from_bytes(array('B', (0x8f, 0x00, 0x7f))) + self.assertEqual(0x80, msg.msg_type) + self.assertEqual('NOTE_OFF', msg.msg_type_str) + self.assertEqual(16, msg.channel) + self.assertEqual(0, msg.data0) + self.assertEqual(127, msg.data1) + self.assertEqual((0x8f, 0x00, 0x7f), msg.as_tuple()) + + def test_equality(self): + m1 = MidiMsg.create_from_bytes(array('B', (0x92, 0x00, 0x7f))) + m2 = MidiMsg.create(0x92, 0x00, 0x7f) + self.assertEqual(m1, m2) + self.assertIsNot(m1, m2) diff --git a/xplane.py b/xplane.py new file mode 100644 index 0000000..c592e69 --- /dev/null +++ b/xplane.py @@ -0,0 +1,61 @@ +import array +import socket +import sys +from collections import namedtuple +from struct import unpack +from typing import Iterator + +DataItem = namedtuple('DataItem', ('index', 'values')) + + +def parse_raw(b: bytes, offset: int = 4) -> str: + return b.decode('latin')[:offset] + ' ' + ' '.join(f'{c:02x}' for c in b[offset:]) + + +def str_dataitem(di: DataItem) -> str: + values = ' '.join(f'{v:10.4f}' for v in di.values) + return f'[{di.index:3d}] {values}' + + +def parse_data_packet(b: bytes) -> Iterator[DataItem]: + data_header_offset = 5 + bytes_per_item = 4 * (8+1) + + if not b.startswith(b'DATA*'): + raw_str = parse_raw(b, offset=5) + raise ValueError(f'invalid DATA* packet: {raw_str}') + + len_b = len(b) + if (len_b - data_header_offset) % bytes_per_item != 0: + raw_str = parse_raw(b, data_header_offset) + raise ValueError(f'improperly aligned DATA* packet: [len: {len_b}] {raw_str}') + + i = 5 + while i < len_b: + data = unpack('iffffffff', b[i:i+bytes_per_item]) + yield DataItem(data[0], data[1:]) + i += bytes_per_item + + +class XPlaneInput: + def __init__(self, addr: str, port: int): + self.s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.s.bind((addr, port)) + + def run(self): + try: + old_data = None + while True: + data = self.s.recv(4096) + assert data.startswith(b"DATA"), data + + data = list(parse_data_packet(data)) + if data != old_data: + print('---') + for di in data: + print(str_dataitem(di)) + + old_data = data + except KeyboardInterrupt: + print("XPlaneInput -> CTRL+C", file=sys.stderr) +