diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
new file mode 100644
index 0000000..2f1ffa0
--- /dev/null
+++ b/.github/workflows/pytest.yml
@@ -0,0 +1,28 @@
+name: pytest
+
+on: [push]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v1
+ - name: Set up Python 3.7
+ uses: actions/setup-python@v1
+ with:
+ python-version: 3.7
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ wget https://github.com/sccn/liblsl/releases/download/1.13.0-b13/liblsl-1.13.0-Linux64-bionic.deb
+ sudo dpkg -i liblsl-1.13.0-Linux64-bionic.deb
+ pip install -U pip
+ pip install git+https://github.com/labstreaminglayer/liblsl-Python.git
+ pip install -r requirements.txt
+ pip install .
+ - name: Test with pytest
+ run: |
+ pip install pytest
+ pytest
diff --git a/.gitignore b/.gitignore
index 7ba05b9..09b1a92 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,6 @@
*/*_pycache_*
*/*.mypy_cache*
*.mypy_cache*
+mypy.ini
+.vscode*
+lib/*
\ No newline at end of file
diff --git a/lib/standard_1005.elc b/lib/standard_1005.elc
deleted file mode 100644
index 4e69532..0000000
--- a/lib/standard_1005.elc
+++ /dev/null
@@ -1,698 +0,0 @@
-# ASA electrode file
-ReferenceLabel avg
-UnitPosition mm
-NumberPositions= 346
-Positions
--86.0761 -19.9897 -47.9860
-85.7939 -20.0093 -48.0310
-0.0083 86.8110 -39.9830
--29.4367 83.9171 -6.9900
-0.1123 88.2470 -1.7130
-29.8723 84.8959 -7.0800
--48.9708 64.0872 -47.6830
--54.8397 68.5722 -10.5900
--45.4307 72.8622 5.9780
--33.7007 76.8371 21.2270
--18.4717 79.9041 32.7520
-0.2313 80.7710 35.4170
-19.8203 80.3019 32.7640
-35.7123 77.7259 21.9560
-46.5843 73.8078 6.0340
-55.7433 69.6568 -10.7550
-50.4352 63.8698 -48.0050
--70.1019 41.6523 -49.9520
--70.2629 42.4743 -11.4200
--64.4658 48.0353 16.9210
--50.2438 53.1112 42.1920
--27.4958 56.9311 60.3420
-0.3122 58.5120 66.4620
-29.5142 57.6019 59.5400
-51.8362 54.3048 40.8140
-67.9142 49.8297 16.3670
-73.0431 44.4217 -12.0000
-72.1141 42.0667 -50.4520
--84.0759 14.5673 -50.4290
--80.7750 14.1203 -11.1350
--77.2149 18.6433 24.4600
--60.1819 22.7162 55.5440
--34.0619 26.0111 79.9870
-0.3761 27.3900 88.6680
-34.7841 26.4379 78.8080
-62.2931 23.7228 55.6300
-79.5341 19.9357 24.4380
-81.8151 15.4167 -11.3300
-84.1131 14.3647 -50.5380
--85.8941 -15.8287 -48.2830
--84.1611 -16.0187 -9.3460
--80.2801 -13.7597 29.1600
--65.3581 -11.6317 64.3580
--36.1580 -9.9839 89.7520
-0.4009 -9.1670 100.2440
-37.6720 -9.6241 88.4120
-67.1179 -10.9003 63.5800
-83.4559 -12.7763 29.2080
-85.0799 -15.0203 -9.4900
-85.5599 -16.3613 -48.2710
--85.6192 -46.5147 -45.7070
--84.8302 -46.0217 -7.0560
--79.5922 -46.5507 30.9490
--63.5562 -47.0088 65.6240
--35.5131 -47.2919 91.3150
-0.3858 -47.3180 99.4320
-38.3838 -47.0731 90.6950
-66.6118 -46.6372 65.5800
-83.3218 -46.1013 31.2060
-85.5488 -45.5453 -7.1300
-86.1618 -47.0353 -45.8690
--73.0093 -73.7657 -40.9980
--72.4343 -73.4527 -2.4870
--67.2723 -76.2907 28.3820
--53.0073 -78.7878 55.9400
--28.6203 -80.5249 75.4360
-0.3247 -81.1150 82.6150
-31.9197 -80.4871 76.7160
-55.6667 -78.5602 56.5610
-67.8877 -75.9043 28.0910
-73.0557 -73.0683 -2.5400
-73.8947 -74.3903 -41.2200
--54.9104 -98.0448 -35.4650
--54.8404 -97.5279 2.7920
--48.4244 -99.3408 21.5990
--36.5114 -100.8529 37.1670
--18.9724 -101.7680 46.5360
-0.2156 -102.1780 50.6080
-19.8776 -101.7930 46.3930
-36.7816 -100.8491 36.3970
-49.8196 -99.4461 21.7270
-55.6666 -97.6251 2.7300
-54.9876 -98.0911 -35.5410
--29.4134 -112.4490 8.8390
-0.1076 -114.8920 14.6570
-29.8426 -112.1560 8.8000
--29.8184 -114.5700 -29.2160
-0.0045 -118.5650 -23.0780
-29.7416 -114.2600 -29.2560
--43.2897 75.8552 -28.2440
--38.5517 79.9532 -4.9950
--27.9857 82.4591 2.7020
--17.1947 84.8491 10.0270
--5.9317 86.8780 16.2000
-7.1053 87.0740 16.4690
-18.9233 85.5969 11.4430
-28.6443 82.9759 2.8280
-39.3203 80.6868 -4.7250
-43.8223 76.5418 -28.3070
--63.2538 53.8573 -30.3160
--61.3508 58.7992 0.8970
--50.7998 64.0412 23.0890
--34.3157 68.3931 41.1880
--11.4357 70.7561 50.3480
-13.4793 71.2010 51.1750
-36.1833 69.1509 41.2540
-52.3972 65.0708 22.8620
-62.9152 60.0448 0.6300
-64.3342 54.5998 -30.4440
--79.0669 28.0813 -31.2530
--74.4999 31.3003 4.8460
--65.2379 36.4282 36.1440
--44.4098 40.7622 61.6900
--15.4238 43.6600 77.6820
-17.5922 44.0540 77.7880
-45.8532 41.6228 60.6470
-67.1281 37.7998 35.2960
-78.0531 32.9817 4.4830
-80.0971 28.5137 -31.3380
--84.1250 -1.8467 -29.7940
--82.3550 0.8263 8.5790
--74.6920 4.3033 45.3070
--51.0509 7.1772 74.3770
--18.2190 9.0941 92.5290
-18.7870 9.2479 91.5620
-51.8851 7.7978 73.5070
-77.0020 5.3357 45.3500
-83.8880 1.9457 8.5010
-84.1230 -1.8083 -29.6380
--86.9731 -32.2157 -27.8480
--85.5651 -30.6287 11.1530
--76.4071 -29.7307 49.2170
--52.9281 -28.9058 80.3040
--18.3541 -28.3219 98.2200
-20.2199 -28.1481 98.1720
-55.1139 -28.3862 80.4740
-79.0059 -28.9863 49.6280
-85.9999 -29.8203 11.2480
-88.6249 -32.2723 -28.0000
--78.1602 -60.7567 -23.8240
--76.6802 -60.8317 12.8800
--68.1152 -62.9747 47.2520
--46.9142 -64.6908 75.2960
--15.8202 -65.5999 91.1640
-19.4198 -65.5950 92.4050
-50.6738 -64.4822 76.1300
-71.0958 -62.6243 47.3280
-78.5198 -60.4323 12.9020
-78.9027 -60.9553 -23.8050
--64.5973 -87.6558 -19.0140
--62.9593 -87.5028 12.9520
--54.0103 -89.8988 37.3320
--35.8874 -91.6669 55.5040
--12.0474 -92.6069 65.5080
-13.9226 -92.6940 66.9580
-37.7986 -91.6291 56.7330
-54.6087 -89.6402 37.0350
-63.1117 -87.2282 12.8560
-65.0137 -87.8062 -18.9520
--42.8624 -108.0730 -13.1510
--40.1204 -107.1290 12.0610
--31.9514 -108.2520 23.0470
--19.8624 -108.9420 29.7600
--6.9194 -109.2600 32.7100
-6.8036 -109.1630 31.5820
-20.2936 -108.9140 28.9440
-32.1756 -108.2520 22.2550
-41.0976 -107.2450 12.1380
-43.8946 -109.1270 -13.1700
--14.8504 -117.9870 -6.9200
-15.0946 -118.0180 -6.9330
--14.8107 87.2351 -4.4770
-15.1623 88.0910 -4.5510
--54.8298 66.4132 -29.7040
--51.1757 70.8362 -1.7550
--39.6407 74.8671 13.6780
--27.2187 78.7091 28.3750
--9.1977 80.6051 35.1330
-10.4823 80.8650 35.3590
-28.5803 79.3029 28.4700
-40.9403 75.7399 13.8600
-52.0293 71.8468 -1.9200
-55.7542 67.1698 -29.8240
--71.5079 41.1193 -30.8540
--68.5558 45.2843 3.0020
--58.4878 50.6722 30.1920
--39.9798 55.2601 52.6000
--13.3838 57.9021 64.3320
-15.8342 58.4559 64.9920
-41.7942 56.2259 51.4990
-60.0522 52.0858 28.7080
-71.9592 47.1917 2.4750
-72.7981 41.8218 -31.0260
--82.9559 13.3203 -30.8080
--80.1139 16.3903 6.8500
--71.2099 20.8203 41.3240
--48.5119 24.5292 69.1360
--17.3439 27.0241 86.9230
-18.4181 27.2709 86.4370
-49.5481 25.2378 68.4300
-73.2191 22.0067 41.2970
-81.5801 17.6837 6.5640
-83.3711 13.5477 -30.7490
--85.1321 -17.0557 -28.7310
--82.9461 -14.8827 10.0090
--75.2941 -12.6397 47.9040
--51.5811 -10.7548 78.0350
--18.2790 -9.4319 97.3560
-19.6780 -9.3041 95.7060
-53.8059 -10.1442 77.7300
-78.1249 -11.7353 47.8400
-85.1369 -13.9063 9.8900
-86.0999 -17.0883 -28.7560
--84.8102 -47.2457 -26.2200
--82.7042 -46.2977 11.9740
--73.3012 -46.7917 49.1090
--51.0492 -47.1758 80.0160
--17.3542 -47.3419 97.4100
-20.6798 -47.2321 98.0720
-53.9968 -46.8902 80.0770
-76.5498 -46.3733 49.1400
-85.1998 -45.8073 12.1020
-85.4428 -47.2213 -26.1760
--72.1773 -74.6277 -21.5360
--70.1133 -74.8677 12.9990
--61.7283 -77.6238 43.0280
--41.6733 -79.7528 66.7150
--13.9613 -81.0029 81.0030
-17.2977 -80.9810 81.6410
-44.7477 -79.6111 67.6550
-63.6267 -77.3022 43.1190
-72.1037 -74.4993 13.0250
-73.2817 -75.0773 -21.5760
--54.7754 -98.9768 -16.1930
--51.9284 -98.4438 12.3040
--43.3424 -100.1629 30.0090
--28.0074 -101.3610 42.3790
--9.5034 -102.0600 49.4180
-10.2356 -102.0290 48.9420
-28.6476 -101.3901 42.1380
-44.2206 -100.2191 29.8080
-52.8386 -98.5360 12.2500
-55.8596 -99.8940 -16.2080
--14.8054 -115.1000 11.8290
-15.1456 -115.1910 11.8330
--15.1584 -118.2420 -26.0480
-15.1286 -118.1510 -26.0810
--36.1247 72.3801 -45.8520
--43.5117 78.5802 -9.2400
--33.2847 81.2071 -1.1400
--22.3517 83.5621 6.0710
--12.2417 86.1941 14.1880
-0.1703 87.3220 17.4420
-13.6223 86.7579 15.3020
-24.1013 84.3769 7.4330
-33.9133 81.8119 -1.0350
-43.9483 79.2958 -9.3000
-37.7123 72.1679 -46.1970
--59.3398 52.6802 -48.7700
--63.2618 55.9922 -11.1730
--55.8198 61.3962 11.8840
--43.3817 66.3672 32.8110
--23.5817 69.9171 47.2930
-0.2763 71.2800 52.0920
-25.5583 70.5559 47.8270
-45.1522 67.2748 32.7310
-58.0002 62.5998 11.9000
-64.6732 57.2738 -11.4600
-60.6012 52.2668 -49.0380
--78.4839 28.7703 -50.5220
--76.6149 28.6533 -11.5080
--71.5059 33.9263 20.9930
--55.9399 38.7162 49.7880
--30.6548 42.4151 71.0400
-0.3512 44.0740 79.1410
-32.6451 43.1009 70.7950
-57.5042 39.8518 48.8110
-74.2501 35.4997 20.3800
-79.0341 30.3437 -11.9970
-79.9201 28.9417 -50.9140
--87.3620 -0.5147 -49.8370
--82.6680 -0.9417 -10.2840
--80.1330 2.5853 27.3120
--64.1610 5.8313 60.8850
--35.7490 8.3091 85.4590
-0.3911 9.5080 95.5600
-36.0700 8.6519 83.8320
-65.1640 6.6198 60.0520
-81.5440 3.6637 27.2010
-83.1680 0.1817 -10.3640
-85.3930 -0.9523 -49.5200
--86.6321 -31.2377 -47.1780
--85.9331 -31.0927 -8.4740
--81.5431 -30.1727 30.2730
--66.1281 -29.2957 65.8980
--36.9301 -28.5699 91.7340
-0.3959 -28.1630 101.2690
-38.5399 -28.2251 90.9760
-68.8539 -28.6403 66.4100
-84.5529 -29.3783 30.8780
-85.9999 -30.2803 -8.4350
-86.7619 -31.7313 -47.2530
--80.7152 -60.6457 -43.5940
--78.5992 -59.7237 -4.7580
--73.6642 -61.9227 30.3800
--59.4112 -63.9248 62.6720
--32.7283 -65.3199 85.9440
-0.3658 -65.7500 94.0580
-35.8918 -65.1381 85.9800
-62.2558 -63.6152 62.7190
-76.6708 -61.5483 30.5430
-79.3188 -59.3033 -4.8400
-81.5598 -61.2153 -43.8000
--64.5703 -86.4318 -38.3240
--64.5833 -86.2218 0.0330
--58.7123 -88.7048 25.1930
--46.1603 -90.8878 47.4460
--24.6483 -92.2919 62.0760
-0.2727 -92.7580 67.3420
-26.4367 -92.2951 63.1990
-47.1437 -90.7122 47.6780
-60.8127 -88.5042 25.6620
-65.1517 -85.9432 -0.0090
-65.0377 -86.7182 -38.4480
--43.1284 -107.5160 -32.3870
--42.9764 -106.4930 5.7730
--36.2344 -107.7160 17.7500
--25.9844 -108.6160 26.5440
--13.6644 -109.2660 32.8560
-0.1676 -109.2760 32.7900
-13.6506 -109.1060 30.9360
-26.6636 -108.6680 26.4150
-37.7006 -107.8400 18.0690
-43.6696 -106.5990 5.7260
-43.1766 -107.4440 -32.4630
--29.3914 -114.5110 -10.0200
-0.0525 -119.3430 -3.9360
-29.5526 -113.6360 -10.0510
--84.1611 -16.0187 -9.3460
--72.4343 -73.4527 -2.4870
-85.0799 -15.0203 -9.4900
-73.0557 -73.0683 -2.5400
--86.0761 -44.9897 -67.9860
- 85.7939 -45.0093 -68.0310
--86.0761 -24.9897 -67.9860
- 85.7939 -25.0093 -68.0310
-Labels
-LPA
-RPA
-Nz
-Fp1
-Fpz
-Fp2
-AF9
-AF7
-AF5
-AF3
-AF1
-AFz
-AF2
-AF4
-AF6
-AF8
-AF10
-F9
-F7
-F5
-F3
-F1
-Fz
-F2
-F4
-F6
-F8
-F10
-FT9
-FT7
-FC5
-FC3
-FC1
-FCz
-FC2
-FC4
-FC6
-FT8
-FT10
-T9
-T7
-C5
-C3
-C1
-Cz
-C2
-C4
-C6
-T8
-T10
-TP9
-TP7
-CP5
-CP3
-CP1
-CPz
-CP2
-CP4
-CP6
-TP8
-TP10
-P9
-P7
-P5
-P3
-P1
-Pz
-P2
-P4
-P6
-P8
-P10
-PO9
-PO7
-PO5
-PO3
-PO1
-POz
-PO2
-PO4
-PO6
-PO8
-PO10
-O1
-Oz
-O2
-I1
-Iz
-I2
-AFp9h
-AFp7h
-AFp5h
-AFp3h
-AFp1h
-AFp2h
-AFp4h
-AFp6h
-AFp8h
-AFp10h
-AFF9h
-AFF7h
-AFF5h
-AFF3h
-AFF1h
-AFF2h
-AFF4h
-AFF6h
-AFF8h
-AFF10h
-FFT9h
-FFT7h
-FFC5h
-FFC3h
-FFC1h
-FFC2h
-FFC4h
-FFC6h
-FFT8h
-FFT10h
-FTT9h
-FTT7h
-FCC5h
-FCC3h
-FCC1h
-FCC2h
-FCC4h
-FCC6h
-FTT8h
-FTT10h
-TTP9h
-TTP7h
-CCP5h
-CCP3h
-CCP1h
-CCP2h
-CCP4h
-CCP6h
-TTP8h
-TTP10h
-TPP9h
-TPP7h
-CPP5h
-CPP3h
-CPP1h
-CPP2h
-CPP4h
-CPP6h
-TPP8h
-TPP10h
-PPO9h
-PPO7h
-PPO5h
-PPO3h
-PPO1h
-PPO2h
-PPO4h
-PPO6h
-PPO8h
-PPO10h
-POO9h
-POO7h
-POO5h
-POO3h
-POO1h
-POO2h
-POO4h
-POO6h
-POO8h
-POO10h
-OI1h
-OI2h
-Fp1h
-Fp2h
-AF9h
-AF7h
-AF5h
-AF3h
-AF1h
-AF2h
-AF4h
-AF6h
-AF8h
-AF10h
-F9h
-F7h
-F5h
-F3h
-F1h
-F2h
-F4h
-F6h
-F8h
-F10h
-FT9h
-FT7h
-FC5h
-FC3h
-FC1h
-FC2h
-FC4h
-FC6h
-FT8h
-FT10h
-T9h
-T7h
-C5h
-C3h
-C1h
-C2h
-C4h
-C6h
-T8h
-T10h
-TP9h
-TP7h
-CP5h
-CP3h
-CP1h
-CP2h
-CP4h
-CP6h
-TP8h
-TP10h
-P9h
-P7h
-P5h
-P3h
-P1h
-P2h
-P4h
-P6h
-P8h
-P10h
-PO9h
-PO7h
-PO5h
-PO3h
-PO1h
-PO2h
-PO4h
-PO6h
-PO8h
-PO10h
-O1h
-O2h
-I1h
-I2h
-AFp9
-AFp7
-AFp5
-AFp3
-AFp1
-AFpz
-AFp2
-AFp4
-AFp6
-AFp8
-AFp10
-AFF9
-AFF7
-AFF5
-AFF3
-AFF1
-AFFz
-AFF2
-AFF4
-AFF6
-AFF8
-AFF10
-FFT9
-FFT7
-FFC5
-FFC3
-FFC1
-FFCz
-FFC2
-FFC4
-FFC6
-FFT8
-FFT10
-FTT9
-FTT7
-FCC5
-FCC3
-FCC1
-FCCz
-FCC2
-FCC4
-FCC6
-FTT8
-FTT10
-TTP9
-TTP7
-CCP5
-CCP3
-CCP1
-CCPz
-CCP2
-CCP4
-CCP6
-TTP8
-TTP10
-TPP9
-TPP7
-CPP5
-CPP3
-CPP1
-CPPz
-CPP2
-CPP4
-CPP6
-TPP8
-TPP10
-PPO9
-PPO7
-PPO5
-PPO3
-PPO1
-PPOz
-PPO2
-PPO4
-PPO6
-PPO8
-PPO10
-POO9
-POO7
-POO5
-POO3
-POO1
-POOz
-POO2
-POO4
-POO6
-POO8
-POO10
-OI1
-OIz
-OI2
-T3
-T5
-T4
-T6
-M1
-M2
-A1
-A2
diff --git a/localite/.vscode/settings.json b/localite/.vscode/settings.json
deleted file mode 100644
index ef55170..0000000
--- a/localite/.vscode/settings.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "python.pythonPath": "C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\Python37_64\\python.exe"
-}
\ No newline at end of file
diff --git a/localite/_client.py b/localite/_client.py
new file mode 100644
index 0000000..33e02f2
--- /dev/null
+++ b/localite/_client.py
@@ -0,0 +1,323 @@
+# -*- coding: utf-8 -*-
+"""
+A client to communicate with the Localite to control the magventure.
+
+@author: Robert Guggenberger
+"""
+# %%
+import socket
+import json
+import pylsl
+import threading
+import time
+from typing import Callable
+from logging import getLogger
+logger = getLogger("LocaliteClient")
+
+def is_port_in_use(host, port):
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+ return s.connect_ex((host, port)) == 0
+
+
+def get_client(host='127.0.0.1', port=6666):
+ if is_port_in_use("127.0.0.1", port+1):
+ kill_client()
+ client = SmartClient(host=host, port=port)
+ return client.start()
+
+def kill_client(host='127.0.0.1', port=6667):
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+ s.connect((host, port))
+ msg = json.dumps({"command":"die"}).encode("ascii")
+ s.send(msg)
+
+
+
+# %%
+class SmartClient():
+ def __init__(self, name:str="localite_marker", host:str="127.0.0.1", port:int=6666):
+ self.name = name
+ self.port = port
+ self.host = host
+
+ def stop(self):
+ kill_client()
+
+ def start(self):
+ port = self.port
+ host = self.host
+ name = self.name
+ client = ReceiverClient(name=name, host=host, port=port)
+ killer = ReceiverServer(host="127.0.0.1", port=port+1, foo=client.stop)
+ client.start()
+ killer.start()
+ return client
+
+
+# %%
+
+def read_msg(client):
+ 'receive byte for byte to read the header telling the message length'
+ #parse the message until it is a valid json
+ msg = bytearray(b' ')
+ while True:
+ try:
+ prt = client.recv(1)
+ msg += prt
+ return json.loads(msg.decode('ascii')) # because the first byte is b' '
+ except json.decoder.JSONDecodeError:
+ pass
+ except Exception as e:
+ print(e)
+ break
+
+ return None
+
+class ReceiverServer(threading.Thread):
+
+ def __init__(self, host="127.0.0.1", port: int = 6667, foo:Callable=None):
+ threading.Thread.__init__(self)
+ self.host = host
+ self.port = port
+ self.stop_client = foo
+ self.is_running = threading.Event()
+
+ def stop(self):
+ self.is_running.clear()
+
+ def run(self):
+ interface = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ interface.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ interface.settimeout(1)
+ interface.bind((self.host, self.port))
+ interface.listen(1)
+ print('Server managing Localite Clients opened at {0}:{1}'.format(
+ self.host, self.port))
+ self.is_running.set()
+ while self.is_running.is_set():
+ message = {"command":"live"}
+ try:
+ client, address = interface.accept()
+ try:
+ message = read_msg(client)
+ except socket.timeout:
+ print('Client from {address} timed out')
+ finally:
+ client.shutdown(2)
+ client.close()
+ except socket.timeout:
+ pass
+
+ if message["command"] == "die":
+ print("Attempting to stop client")
+ self.stop_client()
+ self.stop()
+ else:
+ print("Shutting down ReceiverServer at", self.host, self.port)
+
+
+
+# %%
+
+
+class ReceiverClient(threading.Thread):
+ "LSL based software marker streamer"
+
+ def __init__(self, name:str="localite_marker", host:str="127.0.0.1", port=6666):
+ threading.Thread.__init__(self)
+ self.host = host
+ self.port = port
+ self.name = name
+
+ self.client_lock = threading.Lock()
+ self.client = Client(host=self.host, port=self.port)
+
+ source_id = '_'.join((socket.gethostname(), name))
+ self.info = pylsl.StreamInfo(self.name, type='Markers', channel_count=1,
+ nominal_srate=0, channel_format='string', source_id=source_id)
+ self.outlet = pylsl.StreamOutlet(self.info)
+ self.outlet_lock = threading.Lock()
+ self.is_running = threading.Event()
+
+ def stop(self):
+ print("Attempting to stop")
+ self.is_running.clear()
+ print("Shutting down ReceiverClient for", self.host, self.port)
+ del self.outlet
+
+ def run(self):
+ print('Client manager listing to localite opened at {0}:{1}'.format(
+ self.host, self.port))
+ self.is_running.set()
+ print(self.info.as_xml())
+ while self.is_running.is_set():
+ try:
+ with self.client_lock:
+ key, val = self.client.listen()
+ except (ConnectionResetError, ConnectionRefusedError):
+ print("Connection Problems. Check that host={self.host} is valid.")
+
+ tstamp = pylsl.local_clock()
+ marker = json.dumps({key: val})
+
+ if key in ('coil_0_didt', 'coil_1_didt'): # localite has triggered
+ print(f'Pushed {marker} at {tstamp}')
+ with self.outlet_lock:
+ self.outlet.push_sample([marker], tstamp)
+ else:
+ print("Shutting down ReceiverClient for", self.host, self.port)
+
+ def send(self, msg: str):
+ with self.client_lock:
+ try:
+ self.client.send(msg)
+ print(f'Send {msg} at {pylsl.local_clock()}')
+ except (ConnectionResetError, ConnectionRefusedError):
+ print("Connection Problems. I'll keep on trying to connect")
+ time.sleep(5)
+ self.client = Client(host=self.host, port=self.port)
+
+ def request(self, msg):
+ try:
+ with self.client_lock:
+ answer = self.client.request(msg)
+ print(f'Received {answer} for {msg} at {pylsl.local_clock()}')
+ return answer
+ except (ConnectionResetError, ConnectionRefusedError):
+ print("Connection Problems. Retry")
+ return None
+
+ def trigger(self, id: str):
+ marker = '{"single_pulse":"COIL_' + id + '"}'
+ try:
+ with self.client_lock:
+ self.client.send(marker)
+ except (ConnectionResetError, ConnectionRefusedError):
+ print("Connection Problems. Aborting for safety")
+ return None
+
+ tstamp = pylsl.local_clock()
+ print(f'Pushed {marker} at {tstamp}')
+ with self.outlet_lock:
+ self.outlet.push_sample([marker], tstamp)
+
+ return tstamp
+
+ def push_marker(self, marker:str):
+
+ tstamp = pylsl.local_clock()
+
+ print(f'Pushed {marker} at {tstamp}')
+
+ with self.outlet_lock:
+
+ self.outlet.push_sample([marker], tstamp)
+
+
+# %%
+
+
+class Client(object):
+ """
+ A LocaliteJSON socket client used to communicate with a LocaliteJSON socket server.
+
+ example
+ -------
+ host = '127.0.0.1'
+ port = 6666
+ client = Client(True)
+ client.connect(host, port).send(data)
+ response = client.recv()
+ client.close()
+
+ example
+ -------
+ response = Client().connect(host, port).send(data).recv_close()
+ """
+
+ socket = None
+
+ def __init__(self, host, port=6666, timeout=None):
+ self.host = host
+ self.port = port
+ self.timeout = timeout
+
+ def connect(self):
+ 'connect wth the remote server'
+ self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.socket.connect((self.host, self.port))
+ self.socket.settimeout(self.timeout)
+
+ def close(self):
+ 'closes the connection'
+ self.socket.shutdown(1)
+ self.socket.close()
+ del self.socket
+
+ def write(self, data):
+ self.socket.sendall(data.encode('ascii'))
+ return self
+
+ def read_byte(self, counter, buffer):
+ """read next byte from the TCP/IP bytestream and decode as ASCII"""
+ if counter is None:
+ counter = 0
+ char = self.socket.recv(1).decode('ASCII')
+ buffer.append(char)
+ counter += {'{': 1, '}': -1}.get(char, 0)
+ return counter, buffer
+
+ def read(self):
+ 'parse the message until it is a valid json'
+ counter = None
+ buffer = []
+ while counter is not 0:
+ counter, buffer = self.read_byte(counter, buffer)
+ response = ''.join(buffer)
+ return self.decode(response)
+
+ def listen(self):
+ self.connect()
+ msg = self.read()
+ self.close()
+ return msg
+
+ def decode(self, msg: str, index=0):
+ # catches and interface error
+ msg = msg.replace('reason', '\"reason\"')
+ try:
+ decoded = json.loads(msg)
+ except json.JSONDecodeError as e:
+ print("JSONDecodeError: " + msg)
+ raise e
+
+ key = list(decoded.keys())[index]
+ val = decoded[key]
+ return key, val
+
+ def send(self, msg: str):
+ self.connect()
+ self.write(msg)
+ self.close()
+
+ def request(self, msg="coil_0_amplitude"):
+ self.connect()
+ msg = '{"get":\"' + msg + '\"}'
+ self.write(msg)
+ key = val = ''
+ _, expected = self.decode(msg)
+ logger.debug(msg)
+ logger.debug(expected)
+ while key != expected:
+ key, val = self.read()
+ self.close()
+ return None if val == 'NONE' else val
+
+
+# c = Client(host=host)
+# %timeit -n 1 -r 1000 c.request()
+# 5.22 ms ± 3.29 ms per loop (mean ± std. dev. of 1000 runs, 1 loop each)
+if __name__ == "__main__":
+
+ client = get_client(host="134.2.117.173")
+
diff --git a/localite/client.py b/localite/client.py
index 33e02f2..ca9d5f6 100644
--- a/localite/client.py
+++ b/localite/client.py
@@ -1,323 +1,67 @@
-# -*- coding: utf-8 -*-
-"""
-A client to communicate with the Localite to control the magventure.
-
-@author: Robert Guggenberger
-"""
-# %%
import socket
+from pylsl import local_clock
import json
-import pylsl
-import threading
-import time
-from typing import Callable
-from logging import getLogger
-logger = getLogger("LocaliteClient")
-
-def is_port_in_use(host, port):
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
- return s.connect_ex((host, port)) == 0
-
-
-def get_client(host='127.0.0.1', port=6666):
- if is_port_in_use("127.0.0.1", port+1):
- kill_client()
- client = SmartClient(host=host, port=port)
- return client.start()
-
-def kill_client(host='127.0.0.1', port=6667):
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
- s.connect((host, port))
- msg = json.dumps({"command":"die"}).encode("ascii")
- s.send(msg)
-
-
-
-# %%
-class SmartClient():
- def __init__(self, name:str="localite_marker", host:str="127.0.0.1", port:int=6666):
- self.name = name
- self.port = port
- self.host = host
-
- def stop(self):
- kill_client()
-
- def start(self):
- port = self.port
- host = self.host
- name = self.name
- client = ReceiverClient(name=name, host=host, port=port)
- killer = ReceiverServer(host="127.0.0.1", port=port+1, foo=client.stop)
- client.start()
- killer.start()
- return client
-
-
-# %%
-
-def read_msg(client):
- 'receive byte for byte to read the header telling the message length'
- #parse the message until it is a valid json
- msg = bytearray(b' ')
- while True:
- try:
- prt = client.recv(1)
- msg += prt
- return json.loads(msg.decode('ascii')) # because the first byte is b' '
- except json.decoder.JSONDecodeError:
- pass
- except Exception as e:
- print(e)
- break
-
- return None
-
-class ReceiverServer(threading.Thread):
-
- def __init__(self, host="127.0.0.1", port: int = 6667, foo:Callable=None):
- threading.Thread.__init__(self)
- self.host = host
- self.port = port
- self.stop_client = foo
- self.is_running = threading.Event()
-
- def stop(self):
- self.is_running.clear()
-
- def run(self):
- interface = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- interface.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- interface.settimeout(1)
- interface.bind((self.host, self.port))
- interface.listen(1)
- print('Server managing Localite Clients opened at {0}:{1}'.format(
- self.host, self.port))
- self.is_running.set()
- while self.is_running.is_set():
- message = {"command":"live"}
- try:
- client, address = interface.accept()
- try:
- message = read_msg(client)
- except socket.timeout:
- print('Client from {address} timed out')
- finally:
- client.shutdown(2)
- client.close()
- except socket.timeout:
- pass
-
- if message["command"] == "die":
- print("Attempting to stop client")
- self.stop_client()
- self.stop()
- else:
- print("Shutting down ReceiverServer at", self.host, self.port)
+from typing import Dict, Any
+class Client:
+ "Basic Client communicating with the MarkerServer"
-# %%
-
-
-class ReceiverClient(threading.Thread):
- "LSL based software marker streamer"
-
- def __init__(self, name:str="localite_marker", host:str="127.0.0.1", port=6666):
- threading.Thread.__init__(self)
+ def __init__(self, host="127.0.0.1", port: int = 6667, verbose=True):
self.host = host
self.port = port
- self.name = name
-
- self.client_lock = threading.Lock()
- self.client = Client(host=self.host, port=self.port)
-
- source_id = '_'.join((socket.gethostname(), name))
- self.info = pylsl.StreamInfo(self.name, type='Markers', channel_count=1,
- nominal_srate=0, channel_format='string', source_id=source_id)
- self.outlet = pylsl.StreamOutlet(self.info)
- self.outlet_lock = threading.Lock()
- self.is_running = threading.Event()
-
- def stop(self):
- print("Attempting to stop")
- self.is_running.clear()
- print("Shutting down ReceiverClient for", self.host, self.port)
- del self.outlet
-
- def run(self):
- print('Client manager listing to localite opened at {0}:{1}'.format(
- self.host, self.port))
- self.is_running.set()
- print(self.info.as_xml())
- while self.is_running.is_set():
- try:
- with self.client_lock:
- key, val = self.client.listen()
- except (ConnectionResetError, ConnectionRefusedError):
- print("Connection Problems. Check that host={self.host} is valid.")
-
- tstamp = pylsl.local_clock()
- marker = json.dumps({key: val})
-
- if key in ('coil_0_didt', 'coil_1_didt'): # localite has triggered
- print(f'Pushed {marker} at {tstamp}')
- with self.outlet_lock:
- self.outlet.push_sample([marker], tstamp)
- else:
- print("Shutting down ReceiverClient for", self.host, self.port)
-
- def send(self, msg: str):
- with self.client_lock:
- try:
- self.client.send(msg)
- print(f'Send {msg} at {pylsl.local_clock()}')
- except (ConnectionResetError, ConnectionRefusedError):
- print("Connection Problems. I'll keep on trying to connect")
- time.sleep(5)
- self.client = Client(host=self.host, port=self.port)
-
- def request(self, msg):
- try:
- with self.client_lock:
- answer = self.client.request(msg)
- print(f'Received {answer} for {msg} at {pylsl.local_clock()}')
- return answer
- except (ConnectionResetError, ConnectionRefusedError):
- print("Connection Problems. Retry")
- return None
-
- def trigger(self, id: str):
- marker = '{"single_pulse":"COIL_' + id + '"}'
- try:
- with self.client_lock:
- self.client.send(marker)
- except (ConnectionResetError, ConnectionRefusedError):
- print("Connection Problems. Aborting for safety")
- return None
-
- tstamp = pylsl.local_clock()
- print(f'Pushed {marker} at {tstamp}')
- with self.outlet_lock:
- self.outlet.push_sample([marker], tstamp)
+ self.verbose = verbose
- return tstamp
-
- def push_marker(self, marker:str):
-
- tstamp = pylsl.local_clock()
-
- print(f'Pushed {marker} at {tstamp}')
-
- with self.outlet_lock:
-
- self.outlet.push_sample([marker], tstamp)
-
-
-# %%
-
-
-class Client(object):
- """
- A LocaliteJSON socket client used to communicate with a LocaliteJSON socket server.
-
- example
- -------
- host = '127.0.0.1'
- port = 6666
- client = Client(True)
- client.connect(host, port).send(data)
- response = client.recv()
- client.close()
-
- example
- -------
- response = Client().connect(host, port).send(data).recv_close()
- """
-
- socket = None
-
- def __init__(self, host, port=6666, timeout=None):
- self.host = host
- self.port = port
- self.timeout = timeout
+ def push(self, marker: Dict[str, Any] = {"command": "ping"},
+ tstamp: float = None):
+ "connects, sends a message, and closes the connection"
+ self.connect()
+ self.write(marker, tstamp)
+ self.close()
def connect(self):
- 'connect wth the remote server'
- self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- self.socket.connect((self.host, self.port))
- self.socket.settimeout(self.timeout)
+ "connect wth the remote server"
+ self.interface = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.interface.connect((self.host, self.port))
+ self.interface.settimeout(1)
+
+ def write(self, marker, tstamp):
+ "encode message into ascii and send all bytes"
+ msg = json.dumps((marker, tstamp)).encode("ascii")
+ if self.verbose:
+ print(f"Sending {marker} at {tstamp}")
+ self.interface.sendall(msg)
def close(self):
- 'closes the connection'
- self.socket.shutdown(1)
- self.socket.close()
- del self.socket
-
- def write(self, data):
- self.socket.sendall(data.encode('ascii'))
- return self
-
- def read_byte(self, counter, buffer):
- """read next byte from the TCP/IP bytestream and decode as ASCII"""
- if counter is None:
- counter = 0
- char = self.socket.recv(1).decode('ASCII')
- buffer.append(char)
- counter += {'{': 1, '}': -1}.get(char, 0)
- return counter, buffer
-
- def read(self):
- 'parse the message until it is a valid json'
- counter = None
- buffer = []
- while counter is not 0:
- counter, buffer = self.read_byte(counter, buffer)
- response = ''.join(buffer)
- return self.decode(response)
-
- def listen(self):
- self.connect()
- msg = self.read()
- self.close()
- return msg
+ "closes the connection"
+ self.interface.shutdown(1)
+ self.interface.close()
- def decode(self, msg: str, index=0):
- # catches and interface error
- msg = msg.replace('reason', '\"reason\"')
- try:
- decoded = json.loads(msg)
- except json.JSONDecodeError as e:
- print("JSONDecodeError: " + msg)
- raise e
- key = list(decoded.keys())[index]
- val = decoded[key]
- return key, val
+def available(port: int = 6667, host: str = "127.0.0.1", verbose=True) -> bool:
+ """test whether a markerserver is already available at port
- def send(self, msg: str):
- self.connect()
- self.write(msg)
- self.close()
+ args
+ ----
- def request(self, msg="coil_0_amplitude"):
- self.connect()
- msg = '{"get":\"' + msg + '\"}'
- self.write(msg)
- key = val = ''
- _, expected = self.decode(msg)
- logger.debug(msg)
- logger.debug(expected)
- while key != expected:
- key, val = self.read()
- self.close()
- return None if val == 'NONE' else val
+ host: str
+ the ip of the markerserver (defaults to localhost)
+ port: int
+ the port number of the markerserver (defaults to 6667)
-# c = Client(host=host)
-# %timeit -n 1 -r 1000 c.request()
-# 5.22 ms ± 3.29 ms per loop (mean ± std. dev. of 1000 runs, 1 loop each)
-if __name__ == "__main__":
+ returns
+ -------
- client = get_client(host="134.2.117.173")
-
+ status: bool
+ True if available, False if not
+ """
+ c = Client(host=host, port=port)
+ try:
+ c.push({"command": "ping"}, local_clock())
+ return True
+ except ConnectionRefusedError as e:
+ if verbose:
+ print(e)
+ print(f"Markerserver at {host}:{port} is not available")
+ return False
diff --git a/localite/clients/__init__.py b/localite/clients/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/localite/coil.py b/localite/coil.py
index 0cff8a1..fa31761 100644
--- a/localite/coil.py
+++ b/localite/coil.py
@@ -5,7 +5,7 @@
@author: Robert Guggenberger
"""
-from client import get_client
+from localite.client import get_client
from json import dumps as _dumps
# %%
diff --git a/localite/mitm.py b/localite/mitm.py
new file mode 100644
index 0000000..d5d4468
--- /dev/null
+++ b/localite/mitm.py
@@ -0,0 +1,200 @@
+from pylsl import StreamInfo, StreamOutlet
+import pylsl
+import socket
+import threading
+import queue
+import json
+from time import sleep
+import pkg_resources
+
+
+def myip() -> str:
+ """returns a string with the computers default IP address
+ """
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ s.connect(("8.8.8.8", 80))
+ ip = s.getsockname()[0]
+ s.close()
+ return ip
+
+
+# -----------------------------------------------------------------------------
+def _read_msg(client):
+ "receive byte for byte to read the header telling the message length"
+ # parse the message until it is a valid json
+ msg = bytearray(b" ")
+ while True:
+ try:
+ prt = client.recv(1)
+ msg += prt
+ # because the first byte is b' '
+ marker, tstamp = json.loads(msg.decode("ascii"))
+ print(f"Received {marker} for {tstamp} at {pylsl.local_clock()}")
+ return marker, tstamp
+ except json.decoder.JSONDecodeError:
+ pass
+ except Exception as e:
+ print(e)
+ break
+
+ return ("", None)
+
+
+class ManInTHeMiddle(threading.Thread):
+ """Main class to manage the LSL-MarkerStream as man-in-the-middle
+
+ when started, it automatically checks whether there is already a MarkerServer
+ running at that port. If this is the case, it returns and lets the old one
+ keep control. This ensures that subscribers to the old MarkerServer
+ don't experience any hiccups.
+ """
+
+ def __init__(
+ self,
+ own_host: str = "127.0.0.1",
+ own_port: int = 6667,
+ localite_host: str = "127.0.0.1",
+ localite_port: int = 6666,
+ ):
+ self.localite_host = localite_host
+ self.localite_port = localite_port
+ self.own_host = own_host
+ self.own_port = own_port
+ self.is_running = threading.Event()
+ self.is_running.clear()
+
+ def stop(self):
+ "stop the server"
+ self.is_running.clear()
+
+ def run(self):
+ """wait for clients to connect and send messages.
+
+ This is a Thread, so start with :meth:`server.start`
+ """
+
+ # we check whether there is already an instance running, and if so
+ # let it keep control by returning
+ if available(self.port):
+ self.singleton.clear()
+ if self.verbose:
+ print("Server already running on that port")
+ self.is_running.set()
+ return
+ else:
+ self.singleton.set()
+ if self.verbose:
+ print("This server is the original instance")
+
+ # create the MarkerStreamer, i.e. the LSL-Server that distributes the strings received from the Listener
+ markerstreamer = _MarkerStreamer(name=self.name)
+ markerstreamer.start()
+ # create the ListenerServer, i.e. the TCP/IP Server that waits for messages for forwarding them to the MarkerStreamer
+ listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ listener.settimeout(1)
+ listener.bind((self.host, self.port))
+ listener.listen(1)
+ if self.verbose:
+ print(
+ "Server mediating an LSL Outlet opened at {0}:{1}".format(
+ self.host, self.port
+ )
+ )
+ self.is_running.set()
+ while self.is_running.is_set():
+ try:
+ client, address = listener.accept()
+ try:
+ marker, tstamp = _read_msg(client)
+ if marker.lower() == "ping": # connection was only pinged
+ print("Received ping from", address)
+ elif marker.lower() == "poison-pill":
+ print("Swallowing poison pill")
+ self.is_running.clear()
+ break
+ else:
+ markerstreamer.push(marker, tstamp)
+ except socket.timeout:
+ print("Client from {address} timed out")
+ finally:
+ client.shutdown(2)
+ client.close()
+ except socket.timeout:
+ pass
+
+ print(f"Shutting down MarkerServer: {self.name}")
+ markerstreamer.stop()
+
+
+def create_outlet(name: str = "localite_markers") -> [StreamOutlet, StreamInfo]:
+ """Create a Marker StreamOutlet with the given name.
+
+ Raise an ConnectionAbortedError if the source_id is already in use somewhere else
+
+ """
+ source_id = "_".join((socket.gethostname(), name))
+ info = StreamInfo(
+ name,
+ type="Markers",
+ channel_count=1,
+ nominal_srate=0,
+ channel_format="string",
+ source_id=source_id,
+ )
+
+ info.desc().append_child_value("software", "localite TMS Navigator 4.0")
+ info.desc().append_child_value("stimulator", "MagVenture")
+ info.desc().append_child_value("streamer", str(
+ pkg_resources.get_distribution("localite")))
+
+ if pylsl.resolve_byprop("source_id", source_id, timeout=3):
+ raise ConnectionAbortedError(
+ f"There is already a localiteLSL with the same source_id {source_id} running"
+ )
+ outlet = StreamOutlet(info)
+ return outlet, info
+
+
+class MarkerStreamer(threading.Thread):
+ "publishes whatever is in its queue as a MarkerStream"
+
+ def __init__(self, name: str = "localite_marker"):
+ threading.Thread.__init__(self)
+ self.queue = queue.Queue(maxsize=0) # indefinite size
+ self.is_running = threading.Event()
+ self.name = name
+
+ def push(self, marker: str = "", tstamp: float = None):
+ if marker == "":
+ return
+ if tstamp is None:
+ tstamp = pylsl.local_clock()
+ self.queue.put_nowait((marker, tstamp))
+
+ def stop(self):
+ self.queue.join()
+ self.is_running.clear()
+
+ def await_running(self):
+ print("[", end="")
+ while not self.is_running.is_set():
+ print(".", end="")
+ sleep(0.5)
+ print("]")
+
+ def run(self):
+ outlet, info = create_outlet(name=self.name)
+ self.is_running.set()
+ print("Starting MarkerStreamer")
+ print(info.as_xml())
+ while self.is_running.is_set():
+ try:
+ marker, tstamp = self.queue.get(block=False)
+ outlet.push_sample([marker], tstamp)
+ self.queue.task_done()
+ print(
+ f"Pushed {marker} from {tstamp} at {pylsl.local_clock()}")
+ except queue.Empty:
+ sleep(0.001)
+ print(f"Shutting down MarkerStreamer: {self.name}")
diff --git a/requirements.txt b/requirements.txt
index 49568c3..dff92d7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1 @@
-json
-socket
\ No newline at end of file
+pylsl >= 1.13
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 120dc31..8352396 100644
--- a/setup.py
+++ b/setup.py
@@ -1,35 +1,41 @@
+import setuptools
from distutils.core import setup
+from pathlib import Path
+with (Path(__file__).parent / "readme.md").open("r") as f:
+ long_description = f.read()
setup(
- name='ctrl-localite',
- version='0.0.3',
- description='Control Magventure with LocaliteJSON',
- long_description='Toolbox to control a Magventure TMS with localites JSON-TCP-IP Interface',
- author='Robert Guggenberger',
- author_email='robert.guggenberger@uni-tuebingen.de',
- url='https://github.com/pyreiz/ctrl-localite',
- download_url='https://github.com/pyreiz/ctrl-localite',
- license='MIT',
- packages=['localite'],
- entry_points={'console_scripts': [
- 'localiteLSL=localite.clients.lsl_client:main',
- 'localiteMock=localite.test.mockserver:mock'],
+ name="localite",
+ version="0.1.0",
+ description="Stream from and control TMS using Localite",
+ long_description=long_description,
+ author="Robert Guggenberger",
+ author_email="robert.guggenberger@uni-tuebingen.de",
+ url="https://github.com/pyreiz/ctrl-localite",
+ download_url="https://github.com/pyreiz/ctrl-localite",
+ license="MIT",
+ packages=setuptools.find_packages(),
+ entry_points={
+ "console_scripts": [
+ "localite-stream=localite.cli:main",
+ "localite-mock=localite.mock:main",
+ ],
},
classifiers=[
- 'Development Status :: 4 - Beta',
- 'Environment :: Console',
- 'Intended Audience :: Developers',
- 'Intended Audience :: Education',
- 'Intended Audience :: Healthcare Industry',
- 'Intended Audience :: Science/Research',
- 'Intended Audience :: Information Technology',
- 'License :: OSI Approved :: MIT License',
- 'Operating System :: OS Independent',
- 'Programming Language :: Python',
- 'Programming Language :: Python :: 3',
- 'Topic :: Scientific/Engineering :: Human Machine Interfaces',
- 'Topic :: Scientific/Engineering :: Medical Science Apps.',
- 'Topic :: Software Development :: Libraries',
- ]
+ "Development Status :: 4 - Beta",
+ "Environment :: Console",
+ "Intended Audience :: Developers",
+ "Intended Audience :: Education",
+ "Intended Audience :: Healthcare Industry",
+ "Intended Audience :: Science/Research",
+ "Intended Audience :: Information Technology",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Topic :: Scientific/Engineering :: Human Machine Interfaces",
+ "Topic :: Scientific/Engineering :: Medical Science Apps.",
+ "Topic :: Software Development :: Libraries",
+ ],
)
diff --git a/localite/test/mockserver.py b/test/mockserver.py
similarity index 100%
rename from localite/test/mockserver.py
rename to test/mockserver.py
diff --git a/localite/test/set_response.py b/test/set_response.py
similarity index 100%
rename from localite/test/set_response.py
rename to test/set_response.py
diff --git a/test/test_streamer.py b/test/test_streamer.py
new file mode 100644
index 0000000..99279b4
--- /dev/null
+++ b/test/test_streamer.py
@@ -0,0 +1,28 @@
+from localite.mitm import MarkerStreamer, create_outlet
+from pylsl import resolve_streams
+from pytest import fixture
+import pkg_resources
+import time
+
+
+@fixture
+def ms(capsys):
+ ms = MarkerStreamer()
+ yield ms
+ ms.stop()
+ time.sleep(1) # shut down within one second
+ out, err = capsys.readouterr()
+ assert "Shutting down" in out
+
+
+def test_outlet(ms, capsys):
+ t0 = time.time()
+ ms.start()
+ ms.await_running()
+ assert (time.time()-t0) < 5 # start within 5 seconds
+ out, err = capsys.readouterr()
+ assert "localite_marker" in out
+ assert "Markers" in out
+ v = str(
+ pkg_resources.get_distribution("localite"))
+ assert f"{v}" in out