├── servercfg.json ├── wlancred.py ├── resources └── images │ ├── MgmtAPIPorts.jpg │ ├── MiPyAlpacaFiles.jpg │ ├── SwExample1BBoard.jpg │ ├── SwExample1NINA.jpg │ ├── SwExample2BBoard.jpg │ └── SwExample2NINA.jpg ├── switch0_expl1.json ├── templates ├── setupswitch0.html └── mipysetup.html ├── switchExample1.py ├── switch0_expl2.json ├── switchExample2.py ├── mipyalpaca ├── alpacadevice.py ├── mipyalpacaswitch.py ├── alpacaswitch.py └── alpacaserver.py ├── .gitignore └── README.md /servercfg.json: -------------------------------------------------------------------------------- 1 | {"discoveryPort": "32227", "serverPort": "20000"} -------------------------------------------------------------------------------- /wlancred.py: -------------------------------------------------------------------------------- 1 | ssid = 'yourSSID' 2 | password = 'yourPassword' 3 | -------------------------------------------------------------------------------- /resources/images/MgmtAPIPorts.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RunTJoe/MiPyAlpaca/HEAD/resources/images/MgmtAPIPorts.jpg -------------------------------------------------------------------------------- /resources/images/MiPyAlpacaFiles.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RunTJoe/MiPyAlpaca/HEAD/resources/images/MiPyAlpacaFiles.jpg -------------------------------------------------------------------------------- /resources/images/SwExample1BBoard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RunTJoe/MiPyAlpaca/HEAD/resources/images/SwExample1BBoard.jpg -------------------------------------------------------------------------------- /resources/images/SwExample1NINA.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RunTJoe/MiPyAlpaca/HEAD/resources/images/SwExample1NINA.jpg -------------------------------------------------------------------------------- /resources/images/SwExample2BBoard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RunTJoe/MiPyAlpaca/HEAD/resources/images/SwExample2BBoard.jpg -------------------------------------------------------------------------------- /resources/images/SwExample2NINA.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RunTJoe/MiPyAlpaca/HEAD/resources/images/SwExample2NINA.jpg -------------------------------------------------------------------------------- /switch0_expl1.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"switchnr": 0,"name": "GPIO OUT","pincfg": {"pin": 15,"pinfct": "OUTP","initval": 1},"canwrite": true,"min": 0,"max": 1,"step": 1,"swfct": "MiPyPin","descr": "Output test pin"}, 3 | {"switchnr": 1,"name": "GPIO In","pincfg": {"pin": 16,"pinfct": "INP","pull": "PULL_DOWN"},"canwrite": false,"min": 0,"max": 1,"step": 1,"swfct": "MiPyPin","descr": "Input test pin"}, 4 | {"switchnr": 2,"name": "PWM test","pincfg": {"pin": 14,"pinfct": "PWM","initval": 19000,"freq": 1000},"canwrite": true,"min": 0,"max": 65535,"step": 1,"swfct": "MiPyPin","descr": "PWM test pin"}, 5 | {"switchnr": 3,"name": "ADC test","pincfg": {"pin": 28,"pinfct": "ADC"},"canwrite": false,"min": 0,"max": 65535,"step": 1,"swfct": "MiPyPin","descr": "ADC test pin"} 6 | ] -------------------------------------------------------------------------------- /templates/setupswitch0.html: -------------------------------------------------------------------------------- 1 | {% args devname, cfgfile %} 2 | 3 | 4 | 5 | 27 | 28 | 29 | 30 | 31 |
32 |
33 |

MiPyAlpaca {{ devname }}

34 |

Setup

35 |

36 | Please edit file {{ cfgfile }} for setup of switch device. 37 |

38 |
39 |
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /switchExample1.py: -------------------------------------------------------------------------------- 1 | # switchExample1: Simple Alpaca switch device 2 | 3 | import uasyncio 4 | import wlancred # contains WLAN SSID and password 5 | from mipyalpaca.alpacaserver import AlpacaServer 6 | from mipyalpaca.mipyalpacaswitch import MiPySwitchDevice 7 | 8 | # Asyncio coroutine 9 | async def main(): 10 | await AlpacaServer.startServer() 11 | 12 | 13 | # Create Alpaca Server 14 | srv = AlpacaServer("MyPicoServer", "RTJoe", "0.91", "Unknown") 15 | 16 | # Install switch device 17 | srv.installDevice("switch", 0, MiPySwitchDevice(0, "Pico W Switch", "2fba39e5-e84b-4d68-8aa5-fae287abc02d", "switch0_expl1.json")) 18 | 19 | # Connect to WLAN 20 | AlpacaServer.connectStationMode(wlancred.ssid, wlancred.password) 21 | 22 | # run main function via asyncio 23 | uasyncio.run(main()) 24 | 25 | -------------------------------------------------------------------------------- /switch0_expl2.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"switchnr": 0,"name": "GPIO OUT","pincfg": {"pin": 15,"pinfct": "OUTP","initval": 1},"canwrite": true,"min": 0,"max": 1,"step": 1,"swfct": "MiPyPin","descr": "Output test pin"}, 3 | {"switchnr": 1,"name": "GPIO In","pincfg": {"pin": 16,"pinfct": "INP","pull": "PULL_DOWN"},"canwrite": false,"min": 0,"max": 1,"step": 1,"swfct": "MiPyPin","descr": "Input test pin"}, 4 | {"switchnr": 2,"name": "PWM test","pincfg": {"pin": 14,"pinfct": "PWM","initval": 19000,"freq": 1000},"canwrite": true,"min": 0,"max": 65535,"step": 1,"swfct": "MiPyPin","descr": "PWM test pin"}, 5 | {"switchnr": 3,"name": "ADC test","pincfg": {"pin": 28,"pinfct": "ADC"},"canwrite": false,"min": 0,"max": 65535,"step": 1,"swfct": "MiPyPin","descr": "ADC test pin"} 6 | {"switchnr": 4,"name": "Temperature", "canwrite": false, "min": -10, "max": 99, "step": 0.01, "swfct":"UserDef", "descr": "Temperature DS18B20"}, 7 | ] 8 | -------------------------------------------------------------------------------- /switchExample2.py: -------------------------------------------------------------------------------- 1 | # switchExample2: Alpaca switch device with temperature sensor 2 | 3 | import onewire 4 | import uasyncio 5 | import wlancred # contains WLAN SSID and password 6 | from mipyalpaca.alpacaserver import AlpacaServer 7 | from mipyalpaca.mipyalpacaswitch import MiPySwitchDevice 8 | from machine import Pin 9 | from onewire import OneWire 10 | from ds18x20 import DS18X20 11 | 12 | curr_temp = 0 # store lates temperature measurement in global variable 13 | 14 | 15 | class ExampleSwitchDevice(MiPySwitchDevice): 16 | def __init__(self, devnr, devname, uniqueid, config_file): 17 | super().__init__(devnr, devname, uniqueid, config_file) 18 | 19 | # get switch value 20 | def getswitchvalue(self, id): 21 | global curr_temp 22 | 23 | if id == 4: 24 | return curr_temp # return current temperature for switch ID 4 25 | else: 26 | return super().getswitchvalue(id) 27 | 28 | 29 | async def appGetTemp(): 30 | global curr_temp 31 | while True: 32 | sensor_ds.convert_temp() # start temperature measurement 33 | await uasyncio.sleep_ms(1000) # wait (at least 750ms) 34 | curr_temp = sensor_ds.read_temp(devices[0]) # read and store latest temperature 35 | await uasyncio.sleep_ms(2000) # 2s until start of next measurement 36 | 37 | 38 | # Asyncio coroutine 39 | async def main(): 40 | uasyncio.create_task(appGetTemp()) 41 | await AlpacaServer.startServer() 42 | 43 | 44 | # Init GPIO, OneWire and DS18B20 45 | one_wire_bus = Pin(11) 46 | sensor_ds = DS18X20(OneWire(one_wire_bus)) 47 | # scan for sensor 48 | devices = sensor_ds.scan() 49 | 50 | # Create Alpaca Server 51 | srv = AlpacaServer("MyPicoServer", "RTJoe", "0.91", "Unknown") 52 | 53 | # Install switch device 54 | srv.installDevice("switch", 0, ExampleSwitchDevice(0, "Pico W Switch", "2fba39e5-e84b-4d68-8aa5-fae287abc02d", "switch0_expl2.json")) 55 | 56 | # Connect to WLAN 57 | AlpacaServer.connectStationMode(wlancred.ssid, wlancred.password) 58 | 59 | # run main function via asyncio 60 | uasyncio.run(main()) 61 | 62 | -------------------------------------------------------------------------------- /mipyalpaca/alpacadevice.py: -------------------------------------------------------------------------------- 1 | from mipyalpaca.alpacaserver import * 2 | 3 | # Base class for all Alpaca devices 4 | class AlpacaDevice: 5 | def __init__(self, devnr, devname, uniqueid): 6 | self.device_nr = devnr # device id 7 | self.name = devname # device name 8 | self.description = "No description" # device description 9 | self.driverinfo = "No driver info" # device driver info 10 | self.interfaceVersion = 0 # interface version 11 | self.driverVersion = 0 # interface version 12 | self.connectedState = False # connection status 13 | self.uniqueid = uniqueid # uid 14 | self.server = None # attached Alpaca Server 15 | 16 | # compose reply 17 | def reply(self, request, value=None, err_nr=0, err_msg=""): 18 | return AlpacaServer.reply(request, value, err_nr, err_msg) 19 | 20 | # set connection status 21 | def PUT_connected(self, request): 22 | val = request.form.get('Connected') 23 | if (val != "True") and (val != "False"): 24 | return "Invalid connection value", 400 25 | self.connectedState = bool(val) 26 | return self.reply(request) 27 | 28 | # get connection status 29 | def GET_connected(self, request): 30 | return self.reply(request, self.connectedState) 31 | 32 | # return device name 33 | def GET_name(self, request): 34 | return self.reply(request, self.name) 35 | 36 | # return device description 37 | def GET_description(self, request): 38 | return self.reply(request, self.description) 39 | 40 | # return device driver info 41 | def GET_driverinfo(self, request): 42 | return self.reply(request, self.driverinfo) 43 | 44 | #return device driver version 45 | def GET_driverversion(self, request): 46 | return self.reply(request, self.driverVersion) 47 | 48 | #return device interface version 49 | def GET_interfaceversion(self, request): 50 | return self.reply(request, self.interfaceVersion) 51 | 52 | #return supported (additional) device actions 53 | def GET_supportedactions(self, request): 54 | return self.reply(request, []) 55 | 56 | # return setup page 57 | def setupRequest(self, request): 58 | return "Setup page" 59 | 60 | 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Template python files 132 | templates/*.py 133 | 134 | # Raspberry Pico libs and IDE stuff 135 | .picowgo 136 | .vscode/ 137 | lib/ -------------------------------------------------------------------------------- /templates/mipysetup.html: -------------------------------------------------------------------------------- 1 | {% args title, tab, srvcfg %} 2 | 3 | 4 | 5 | 6 | 7 | 94 | 95 | 96 | 97 |
98 |
99 |

{{ title }}

100 |

Server settings

101 |
102 | 103 | 104 |

The following devices are configured on this Alpaca Server:

105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | {% for device in tab %} 115 | 116 | 117 | 118 | 119 | 120 | {% endfor %} 121 | 122 |
Device TypeDevice NumberDescription
{{ device["DeviceType"] }}{{ device["DeviceNumber"] }}{{ device["DeviceName"] }}

123 | 124 | 125 |
126 |
127 |
128 |
129 | 130 |
131 |
132 | 133 |
134 |
135 |
136 |
137 |
138 | 139 |
140 |
141 | 142 |
143 |
144 |
145 |
146 | 147 |
148 |
149 |
150 | 151 | 152 | -------------------------------------------------------------------------------- /mipyalpaca/mipyalpacaswitch.py: -------------------------------------------------------------------------------- 1 | from mipyalpaca.alpacaswitch import SwitchDevice 2 | from machine import Pin 3 | from machine import PWM 4 | from machine import ADC 5 | from microdot_utemplate import render_template 6 | 7 | 8 | # MicroPython switch device 9 | # support easy configuration of most common switch functions for MicroPython controllers: 10 | # - GPIO outputs 11 | # - GPIO inputs 12 | # - PWM 13 | # - ADC 14 | class MiPySwitchDevice(SwitchDevice): 15 | 16 | def __init__(self, devnr, devname, uniqueid, config_file): 17 | super().__init__(devnr, devname, uniqueid, config_file) 18 | self.description = "MicroPython Alpaca switch device" 19 | self.swpin = [] 20 | 21 | # configure all MicroPython pins 22 | for i in range(self.maxswitch): 23 | sw = self.switchdescr[i] 24 | 25 | if sw["swfct"] == "MiPyPin": 26 | cfg = sw["pincfg"] 27 | pnr = int(cfg["pin"]) 28 | 29 | if cfg["pinfct"] == "OUTP": 30 | # output pin 31 | p = Pin(pnr, Pin.OUT) 32 | if cfg["initval"] != None: 33 | # set initial value 34 | self.switchValue[i] = int(cfg["initval"]) 35 | p.init(value=int(cfg["initval"])) 36 | p.value(int(cfg["initval"])) 37 | self.swpin.insert(i, p) 38 | 39 | if cfg["pinfct"] == "INP": 40 | # input pin 41 | p = Pin(pnr, Pin.IN) 42 | # setup pullup 43 | if cfg["pull"] == "PULL_UP": 44 | p.init(pull=Pin.PULL_UP) 45 | if cfg["pull"] == "PULL_DOWN": 46 | p.init(pull=Pin.PULL_DOWN) 47 | self.swpin.insert(i, p) 48 | 49 | if cfg["pinfct"] == "PWM": 50 | # PWM pin 51 | p = PWM(Pin(pnr)) 52 | p.freq(int(cfg["freq"])) 53 | if cfg["initval"] != None: 54 | # set initial value 55 | self.switchValue[i] = int(cfg["initval"]) 56 | p.duty_u16(int(cfg["initval"])) 57 | self.swpin.insert(i, p) 58 | 59 | if cfg["pinfct"] == "ADC": 60 | # ADC pin 61 | p = ADC(Pin(pnr)) 62 | self.swpin.insert(i, p) 63 | else: 64 | self.swpin.insert(i, "UserDef") 65 | 66 | 67 | # set switch value 68 | def setswitchvalue(self, id, value): 69 | super().setswitchvalue(id, value) 70 | 71 | sw = self.switchdescr[id] 72 | if sw["swfct"] == "MiPyPin": 73 | cfg = sw["pincfg"] 74 | if cfg["pinfct"] == "OUTP": 75 | self.swpin[id].value(value) 76 | if cfg["pinfct"] == "PWM": 77 | self.swpin[id].duty_u16(int(round(value))) 78 | 79 | 80 | # set (boolean) switch value 81 | def setswitch(self, id, value): 82 | self.setswitchvalue(id, value) 83 | 84 | 85 | # get switch value 86 | def getswitchvalue(self, id): 87 | super().getswitchvalue(id) 88 | sw = self.switchdescr[id] 89 | if sw["swfct"] == "MiPyPin": 90 | cfg = sw["pincfg"] 91 | if cfg["pinfct"] == "ADC": 92 | self.switchValue[id] = self.swpin[id].read_u16() 93 | if cfg["pinfct"] == "INP": 94 | self.switchValue[id] = self.swpin[id].value() 95 | return self.switchValue[id] 96 | 97 | 98 | # get (boolean) switch value 99 | def getswitch(self, id): 100 | super().getswitch(id) 101 | sw = self.switchdescr[id] 102 | if sw["swfct"] == "MiPyPin": 103 | cfg = sw["pincfg"] 104 | if cfg["pinfct"] == "INP": 105 | self.switchValue[id] = self.swpin[id].value() 106 | return bool(self.switchValue[id]) 107 | 108 | 109 | # return setup page 110 | def setupRequest(self, request): 111 | return render_template('setupswitch0.html', devname=self.name, cfgfile=self.configfile) 112 | -------------------------------------------------------------------------------- /mipyalpaca/alpacaswitch.py: -------------------------------------------------------------------------------- 1 | from mipyalpaca.alpacaserver import * 2 | from mipyalpaca.alpacadevice import AlpacaDevice 3 | 4 | # ASCOM Alpaca switch device 5 | class SwitchDevice(AlpacaDevice): 6 | 7 | def __init__(self, devnr, devname, uniqueid, config_file): 8 | super().__init__(devnr, devname, uniqueid) 9 | self.interfaceVersion = 2 # this implementation supports switch interface version 2 10 | self.maxswitch = 0 # number of switches 11 | self.switchdescr = [] # switch configuration0 12 | self.switchValue = [] # current switch values 13 | self.driverinfo = "MicroPython ASCOM Alpaca Switch Driver" # switch driver MiPy 14 | self.driverVersion = "v0.90" # driver version 15 | self.configfile = config_file # name of JSON file with switch config 16 | 17 | self.switchdescr = readJson(self.configfile) # load switch configuration 18 | self.maxswitch = len(self.switchdescr) # get number of switches 19 | # create initial list of switch values 20 | for i in range(self.maxswitch): 21 | self.switchValue.append(0) 22 | 23 | 24 | # get switch id from request 25 | def getSwitchId(self, request): 26 | try: 27 | idarg = getArg(request, "Id") 28 | 29 | # N.I.N.A compatibility workaround 30 | if idarg == None: 31 | idarg = getArg(request, "ID") 32 | 33 | id = int(idarg) 34 | except (ValueError, TypeError): 35 | raise CallArgError("Switch ID invalid") 36 | # range check 37 | if (id < 0) or (id >= self.maxswitch): 38 | raise RangeError("Switch ID out of range or missing") 39 | return id 40 | 41 | 42 | # get switch value (might be overwritten for user specific switches) 43 | def getswitchvalue(self, id): 44 | return self.switchValue[id] 45 | 46 | # request for switch value 47 | def GET_getswitchvalue(self, request): 48 | id = self.getSwitchId(request) 49 | return self.reply(request, self.getswitchvalue(id)) 50 | 51 | # get boolean switch value (might be overwritten for user specific switches) 52 | def getswitch(self, id): 53 | return bool(self.switchValue[id]) 54 | 55 | # request for boolean switch value 56 | def GET_getswitch(self, request): 57 | id = self.getSwitchId(request) 58 | return self.reply(request, self.getswitch(id)) 59 | 60 | # set switch value (might be overwritten for user specific switches) 61 | def setswitchvalue(self, id, value): 62 | self.switchValue[id] = value 63 | 64 | # request for setting switch value 65 | def PUT_setswitchvalue(self, request): 66 | id = self.getSwitchId(request) 67 | # raise exception for non-writable switches 68 | if self.switchdescr[id]["canwrite"] == False: 69 | raise NotImplementedError("Device cannot be written to") 70 | 71 | if request.form.get('Value') is None: 72 | raise CallArgError("Invalid or missing switch value") 73 | 74 | v = float(request.form.get("Value")) 75 | # range check 76 | if (v < self.switchdescr[id]["min"]) or (v > self.switchdescr[id]["max"]): 77 | raise RangeError("Value of switch "+str(id)+" out of range or missing") 78 | 79 | self.setswitchvalue(id, v) 80 | return self.reply(request, "") 81 | 82 | 83 | # set boolean switch value (might be overwritten for user specific switches) 84 | def setswitch(self, id, value): 85 | self.switchValue[id] = round(value) 86 | 87 | # request for setting boolean switch value 88 | def PUT_setswitch(self, request): 89 | id = self.getSwitchId(request) 90 | 91 | # raise exception for non-writable switches 92 | if self.switchdescr[id]["canwrite"] == False: 93 | raise NotImplementedError("Device cannot be written to") 94 | 95 | if request.form.get("State") == "True": 96 | self.setswitch(id, 1) 97 | else: 98 | if request.form.get("State") == "False": 99 | self.setswitch(id, 0) 100 | else: 101 | raise CallArgError("Invalid or missing switch state") 102 | return self.reply(request, "") 103 | 104 | # return number of switches 105 | def GET_maxswitch(self, request): 106 | return self.reply(request, self.maxswitch) 107 | 108 | # return switch name 109 | def GET_getswitchname(self, request): 110 | return self.reply(request, self.switchdescr[int(self.getSwitchId(request))]["name"]) 111 | 112 | # set new switch name 113 | def PUT_setswitchname(self, request): 114 | if request.form.get('Name') is None: 115 | raise CallArgError("Invalid or missing switch name") 116 | self.switchdescr[self.getSwitchId(request)]["name"] = request.form.get('Name') 117 | # write value to config file 118 | writeJson(self.configfile, self.switchdescr) 119 | return self.reply(request) 120 | 121 | # return "canwrite" flag 122 | def GET_canwrite(self, request): 123 | return self.reply(request, self.switchdescr[self.getSwitchId(request)]["canwrite"]) 124 | 125 | # return switch description 126 | def GET_getswitchdescription(self, request): 127 | return self.reply(request, self.switchdescr[self.getSwitchId(request)]["descr"]) 128 | 129 | # return minimum switch value 130 | def GET_minswitchvalue(self, request): 131 | return self.reply(request, self.switchdescr[self.getSwitchId(request)]["min"]) 132 | 133 | # return maximum switch value 134 | def GET_maxswitchvalue(self, request): 135 | return self.reply(request, self.switchdescr[self.getSwitchId(request)]["max"]) 136 | 137 | # return switch step size 138 | def GET_switchstep(self, request): 139 | return self.reply(request, self.switchdescr[self.getSwitchId(request)]["step"]) 140 | 141 | 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MiPyAlpaca MicroPython Alpaca Driver 2 | 3 | ## Introduction 4 | 5 | MiPyAlpaca is an ASCOM Alpaca implementation for MicroPython. Currently it’s supporting only the device type “Switch” but can be extended to other devices (I have planned to add “CoverCalibrator” soon). It also supports the Alpaca discovery protocol. I have tested it on Raspberry Pi Pico W and ESP32-C3. 6 | 7 | 8 | 9 | It can be used to make microcontroller-based projects accessible by astronomy software applications (like e.g. N.I.N.A.) or self-written applications via the standardized and operating system independent ASCOM Alpaca protocol. 10 | 11 | Simple controlling of input/output pins, setting of PWM values and reading of ADC values can be done even without coding. As the provided switchExample1 demonstrates, you just have to edit a JSON configuration file. Configure the pin functionality, start the software and connect your application. 12 | 13 | ```json 14 | [ 15 | {"switchnr": 0,"name": "GPIO OUT","pincfg": {"pin": 15,"pinfct": "OUTP","initval": 1},"canwrite": true,"min": 0,"max": 1,"step": 1,"swfct": "MiPyPin","descr": "Output test pin"}, 16 | {"switchnr": 1,"name": "GPIO In","pincfg": {"pin": 16,"pinfct": "INP","pull": "PULL_DOWN"},"canwrite": false,"min": 0,"max": 1,"step": 1,"swfct": "MiPyPin","descr": "Input test pin"}, 17 | {"switchnr": 2,"name": "PWM test","pincfg": {"pin": 14,"pinfct": "PWM","initval": 19000,"freq": 1000},"canwrite": true,"min": 0,"max": 65535,"step": 1,"swfct": "MiPyPin","descr": "PWM test pin"}, 18 | {"switchnr": 3,"name": "ADC test","pincfg": {"pin": 28,"pinfct": "ADC"},"canwrite": false,"min": 0,"max": 65535,"step": 1,"swfct": "MiPyPin","descr": "ADC test pin"} 19 | ] 20 | ``` 21 | 22 | For the configuration shown above, N.I.N.A will display the following switch device (will be automatically adapted to your pin configuration): 23 | 24 | ![](resources/images/SwExample1NINA.jpg) 25 | 26 | Now you can set the output pins, read input pins, set PWM outputs and read ADC inputs from N.I.N.A.. 27 | 28 | Here is a simple breadboard setup testing the example above on a Raspberry Pico W. You can switch on/off one LED with GPIO output pin, read the state of the button, set the brightness of another LED via PWM and read the voltage at the trim potentiometer. 29 | 30 | ![](resources/images/SwExample1BBoard.jpg) 31 | 32 | 33 | 34 | If you want to control more sophisticated “Switches”, you can derive a subclass of MiPySwitchDevice and just implement the required functionality in the get and set methods to read and write the switch values. See switchExample2 for details. 35 | 36 | 37 | 38 | ## Installation 39 | 40 | Copy the files from the GitHub repository on your device. Folders *mipyalpaca* and *templates* shall be subfolders of the folder containing the main application (like e.g. the example files). 41 | 42 | MiPyAlpaca requires two additional packages: 43 | 44 | - Microdot 45 | (https://github.com/miguelgrinberg/microdot) 46 | 47 | - utemplate 48 | [https://github.com/pfalcon/utemplate](https://github.com/pfalcon/utemplate) 49 | 50 | For installation of these packages see the corresponding documentation. If you use the Thonny IDE (https://thonny.org/), you can also use the Tools/Manage packages function. In Thonny, the file system of my installation looks like that: 51 | 52 | 53 | ![](resources/images/MiPyAlpacaFiles.jpg) 54 | 55 | 56 | 57 | ## Usage 58 | 59 | - The recommended starting point is to use one of the provided examples. 60 | 61 | - Edit WLAN credentials in wlancred.py 62 | 63 | - Assign a new Globally Unique ID (UID) for your device in the call argument of `installDevice `(not absolutely necessary, but recommended) 64 | 65 | - If you want to use another discovery port than 32227 (default), or another port for the Alpaca server, edit the port numbers in servercfg.json. 66 | 67 | - Edit switch0.json for your switch configuration (see below for details) 68 | 69 | - Start the example program 70 | 71 | 72 | 73 | ### Switch configuration 74 | 75 | | Attribute | Description | 76 | | --------- | ----------------------------------------------------------------------------------------------- | 77 | | switchnr | Switch ID | 78 | | name | Switch name | 79 | | canwrite | *true* for writable switch
*false* for read-only switch | 80 | | min | Minumum value | 81 | | max | Maximum value | 82 | | step | Value step size | 83 | | swfct | *MiPyPin* for MicroPython pin (see table below)
*UserDef*  for user defined switch function | 84 | | descr | switch description | 85 | 86 | If "swfct" has value "MiPyPin", attribute "pincfg" has to be defined with the following sub-attributes: 87 | 88 | | Attribute | Description | Remarks | 89 | | --------- | ---------------------------------------------------------------------------------- | ---------------------------------- | 90 | | pinfct | OUTP: Output pin
INP: Input pin
ADC: ADC input pin
PWM: PWM output pin | | 91 | | initval | Initialisation value | for OUTP and PWM only
optional | 92 | | pull | PULL_UP: Pull up
PULL_DOWN: Pull down | for INP only
| 93 | 94 | If "swfct" has value "Userdef", create a new subclass derived from class SwitchDevice and overwrite the following methods with the required code: 95 | 96 | - `getswitchvalue` 97 | 98 | - `getswitch` 99 | 100 | - `setswitchvalue` 101 | 102 | - `setswitch` 103 | 104 | 105 | 106 | ## Alpaca Management API 107 | 108 | ### Main Setup Page 109 | 110 | The Alpaca server main setup page can be called by 111 | 112 | **http://*host*:*port*/setup** 113 | 114 | where *host* is usually the ip address of your device. 115 | 116 | ![](resources/images/MgmtAPIPorts.jpg) 117 | 118 | 119 | 120 | ### Device Specific Setup Page(s) 121 | 122 | The device specific setup pages can be called by 123 | 124 | **http://*host*:*port*/setup/v*apiversion_number*/*device_type*/*device_number*/setup** 125 | 126 | As this implementation currently supports only API version v1 and device type "switch", the correct URL for switch device 0 is 127 | 128 | **http://*host*:*port*/setup/api/v1/switch/0/setup** 129 | 130 | 131 | 132 | ## Examples 133 | 134 | The following example applications are provided: 135 | 136 | ### switchExample1 137 | 138 | This is an example with 4 switches, each for one type of the available MicroPython pins: 139 | 140 | - 1 Input pin 141 | 142 | - 1 Output pin 143 | 144 | - 1 ADC pin 145 | 146 | - 1 PWM pin 147 | 148 | 149 | 150 | The example was already described in the introduction above. 151 | 152 | ### switchExample2 153 | 154 | Example with the 4 switches from switchExample1 and 1 additional userdefined switch 155 | 156 | The user defined switch is a read-only "switch" providing the temperature values of a DS18B20 temperature sensor. 157 | 158 | This example code demonstrates (this is the additional code) how to implement support of a DS18B20 temperature sensor which can be read by the astronomy software: 159 | 160 | ```python 161 | class ExampleSwitchDevice(MiPySwitchDevice): 162 | def __init__(self, devnr, devname, uniqueid, config_file): 163 | super().__init__(devnr, devname, uniqueid, config_file) # get switch value 164 | 165 | def getswitchvalue(self, id): 166 | global curr_temp 167 | 168 | if id == 4: 169 | return curr_temp # return current temperature for switch ID 4 170 | else: 171 | return super().getswitchvalue(id) 172 | 173 | ``` 174 | 175 | The temparature sensor is read cyclically in an Asyncio coroutine `appGetTemp`, started in parallel to the Alpaca server: 176 | 177 | ```python 178 | async def appGetTemp(): 179 | global curr_temp 180 | while True: 181 | sensor_ds.convert_temp() # start temperature measurement 182 | await uasyncio.sleep_ms(1000) # wait (at least 750ms) 183 | curr_temp = sensor_ds.read_temp(devices[0]) # read and store latest temperature 184 | await uasyncio.sleep_ms(2000) # 2s until start of next measurement 185 | 186 | 187 | # Asyncio coroutine 188 | async def main(): 189 | uasyncio.create_task(appGetTemp()) 190 | await AlpacaServer.startServer() 191 | ``` 192 | 193 | 194 | 195 | Here is the switch window of N.I.N.A. for switchExample2 with the temperature gauge: 196 | 197 | ![](resources/images/SwExample2NINA.jpg) 198 | 199 | 200 | 201 | This is a sample breadboard setup for this example: 202 | 203 | ![](resources/images/SwExample2BBoard.jpg) 204 | 205 | 206 | 207 | ## ASCOM Alpaca compliance 208 | 209 | MiPyAlpaca was successfully validated by the Conform Universal [ConformU](https://github.com/ASCOMInitiative/ConformU) conformance validation tool to be Alpaca compliant. 210 | 211 | 212 | 213 | # Acknowledgements 214 | 215 | Many thanks to Miguel Grinberg for providing his [Microdot](https://github.com/miguelgrinberg/microdot) framework. He has also provided excellent and fast support to my questions and remarks. 216 | 217 | Also many thanks to Peter Simpson from the [ASCOM Driver and Application Development Support Forum](https://ascomtalk.groups.io/g/Developer) for answering all my questions regarding the Alpaca protocol. 218 | -------------------------------------------------------------------------------- /mipyalpaca/alpacaserver.py: -------------------------------------------------------------------------------- 1 | import ujson 2 | import uasyncio 3 | import uselect 4 | import socket 5 | from microdot_asyncio import Microdot 6 | from microdot_utemplate import render_template 7 | from microdot_asyncio import Response 8 | import network 9 | 10 | 11 | alpaca_app = Microdot() 12 | 13 | # Read JSON file from filename 14 | def readJson(filename): 15 | with open(filename) as fp: 16 | jdata = ujson.load(fp) 17 | fp.close() 18 | return jdata 19 | 20 | 21 | # Write JSON to file filename 22 | def writeJson(filename, jdata): 23 | with open(filename, "w") as fp: 24 | fp.write(ujson.dumps(jdata)) 25 | fp.close() 26 | 27 | 28 | # Get Argument "key" from request 29 | def getArg(request, key): 30 | if request.method == 'PUT': 31 | # Case sensitive for PUT requests 32 | return request.form.get(key) 33 | else: 34 | # Case insensitive for GET requests 35 | for rkey,val in request.args.items(): 36 | if rkey.lower() == key.lower(): 37 | return request.args.get(rkey) 38 | 39 | 40 | # Alpaca error codes 41 | ALPACA_ERR_OK = 0 42 | ALPACA_ERR_NOT_IMPLEMENTED = 1024 43 | ALPACA_ERR_INVALID_VALUE = 1025 44 | 45 | AlpacaDeviceTypes = ["camera", "covercalibrator", "dome", "filterwheel", "focuser", "observingconditions", "rotator", "safetymonitor", "switch", "telescope"] 46 | 47 | # Command argument exception (results in HTTP code 400) 48 | class CallArgError(Exception): 49 | def __init__(self, message): 50 | super().__init__(message) 51 | self.errnr = ALPACA_ERR_INVALID_VALUE 52 | 53 | # Command not implemented exception (results in HTTP code 200) 54 | class NotImplementedError(Exception): 55 | def __init__(self, message): 56 | super().__init__(message) 57 | self.errnr = ALPACA_ERR_NOT_IMPLEMENTED 58 | 59 | # Argument range error (results in HTTP code 200) 60 | class RangeError(Exception): 61 | def __init__(self, message): 62 | super().__init__(message) 63 | self.errnr = ALPACA_ERR_INVALID_VALUE 64 | 65 | 66 | class AlpacaServer: 67 | __instance = None 68 | config = {} 69 | devices = {} 70 | wlan = None 71 | ServerTransactionID = 1 72 | ServerApiVersions = [1] 73 | ServerName = "" 74 | Manufacturer = "" 75 | ManfVersion = "" 76 | ManfLocation = "" 77 | 78 | 79 | def __new__(cls, *args, **kwargs): 80 | if not AlpacaServer.__instance: 81 | AlpacaServer.__instance = object.__new__(cls) 82 | return AlpacaServer.__instance 83 | 84 | def __init__(self,srv_name, srv_manufacturer, srv_manvvers, srv_manfloc): 85 | AlpacaServer.ServerTransactionID = 1 86 | AlpacaServer.ServerApiVersions = [1] 87 | AlpacaServer.ServerName = srv_name 88 | AlpacaServer.Manufacturer = srv_manufacturer 89 | AlpacaServer.ManfVersion = srv_manvvers 90 | AlpacaServer.ManfLocation = srv_manfloc 91 | for dev in AlpacaDeviceTypes: 92 | AlpacaServer.devices[dev] = [] 93 | 94 | AlpacaServer.config = readJson("servercfg.json") 95 | uasyncio.create_task(appDiscovery(self)) 96 | 97 | 98 | # Create reply for request 99 | @classmethod 100 | def reply(cls, request, value=None, err_nr=0, err_msg="", mngmnt_api=False): 101 | AlpacaServer.ServerTransactionID+=1 # increment server transaction ID 102 | r = {"ServerTransactionID": AlpacaServer.ServerTransactionID, "ErrorNumber": err_nr, "ErrorMessage" : err_msg} 103 | if not mngmnt_api: 104 | # return ClientTransactionID of request 105 | try: 106 | clid = getArg(request, "ClientTransactionID") 107 | r["ClientTransactionID"] = int(clid) 108 | except (ValueError, TypeError): 109 | return r 110 | if err_nr == 0: 111 | r["Value"] = value 112 | return r 113 | 114 | 115 | # install device on Alpaca server 116 | def installDevice(self, dev_type, dev_nr, newdevice): 117 | newdevice.device_nr = dev_nr 118 | AlpacaServer.devices[dev_type].insert(dev_nr, newdevice) 119 | newdevice.server = self 120 | 121 | # return server API versions 122 | @classmethod 123 | def getServerApiVersions(cls): 124 | return AlpacaServer.ServerApiVersions 125 | 126 | # return configured devices 127 | @classmethod 128 | def getConfDevices(cls): 129 | devtab = [] 130 | for key,devtype in AlpacaServer.devices.items(): 131 | for dev in devtype: 132 | devtab.append({"DeviceType":key, "DeviceNumber":dev.device_nr, "DeviceName":dev.description, "UniqueID":dev.uniqueid}) 133 | return devtab 134 | 135 | # return server description 136 | @classmethod 137 | def getServerDescr(cls): 138 | return {"ServerName":AlpacaServer.ServerName, "Manufacturer":AlpacaServer.Manufacturer, "ManufacturerVersion":AlpacaServer.ManfVersion, "Location":AlpacaServer.ManfLocation} 139 | 140 | # call API method from request 141 | @classmethod 142 | def callMethod(cls, dev_type, dev_nr, method, request): 143 | 144 | if dev_type not in AlpacaServer.devices: 145 | return "Device type "+dev_type+" not implemented", 400 146 | if (dev_nr >= len(AlpacaServer.devices[dev_type])) or (dev_nr < 0): 147 | return "Device "+dev_type+" "+str(dev_nr)+" not installed", 400 148 | if not hasattr(AlpacaServer.devices[dev_type][dev_nr], request.method+"_"+method): 149 | return "Device "+dev_type+" "+str(dev_nr)+" not installed", 400 150 | 151 | try: 152 | # call the requested method 153 | return getattr(AlpacaServer.devices[dev_type][dev_nr], request.method+"_"+method)(request) 154 | except CallArgError as e: 155 | return str(e), 400 156 | except RangeError as e: 157 | return AlpacaServer.devices[dev_type][dev_nr].reply(request, "", e.errnr, str(e)) 158 | except NotImplementedError as e: 159 | return AlpacaServer.devices[dev_type][dev_nr].reply(request, "", e.errnr, str(e)) 160 | 161 | 162 | # start Microdot Alpaca server 163 | @classmethod 164 | async def startServer(cls): 165 | await alpaca_app.start_server(port=int(AlpacaServer.config["serverPort"]), debug=True) 166 | 167 | 168 | # connect to WLAN in station mode 169 | @classmethod 170 | def connectStationMode(cls, ssid, password): 171 | wlan = network.WLAN(network.STA_IF) 172 | 173 | if not wlan.isconnected(): 174 | print('Connecting to ' + ssid, end=' ') 175 | wlan.active(True) 176 | wlan.connect(ssid, password) 177 | while not wlan.isconnected(): 178 | pass 179 | print('\nConnected to IP address ' + wlan.ifconfig()[0]) 180 | 181 | 182 | # Start as WLAN access point 183 | @classmethod 184 | def startAccessPoint(cls, ssid, password): 185 | wlan = network.WLAN(network.AP_IF) 186 | 187 | wlan.config(essid=ssid, password=password) 188 | wlan.active(True) 189 | 190 | while wlan.active() == False: 191 | pass 192 | print('\nAccessPoint active, IP address ' + wlan.ifconfig()[0]) 193 | 194 | 195 | # Alpaca discovery daemon 196 | async def appDiscovery(server): 197 | srv = server 198 | s=socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 199 | s.bind(('',int(srv.config["discoveryPort"]))) 200 | 201 | print("Start Discovery") 202 | poller = uselect.poll() 203 | poller.register(s, uselect.POLLIN) 204 | 205 | while True: 206 | # poll for discovery broadcasts 207 | evts = poller.poll(0) 208 | for sock, evt in evts: 209 | if evt and uselect.POLLIN: 210 | # received discovery broadcast 211 | print("Broadcast received") 212 | data, address=s.recvfrom(1024) 213 | # reply with own port 214 | send_data = "{\n \"AlpacaPort\":"+srv.config["serverPort"]+"\n}" 215 | s.sendto(send_data.encode('utf-8'), address) 216 | await uasyncio.sleep_ms(10) 217 | 218 | 219 | 220 | Response.default_content_type = 'text/html' 221 | 222 | # API call 223 | @alpaca_app.route('/api/v1///', methods=['GET', 'PUT']) 224 | async def apicall(request,devtype,devnr,method): 225 | try: # check ClientID 226 | clid_arg = getArg(request, "ClientID") 227 | if clid_arg == None: 228 | return "Invalid ClientID", 400 229 | clid = int(clid_arg) 230 | except (ValueError, TypeError): 231 | return "Invalid ClientID", 400 232 | 233 | try: # check ClientTransactionID 234 | trid_arg = getArg(request, "ClientTransactionID") 235 | if trid_arg == None: 236 | return "Invalid ClientTransactionID", 400 237 | trid = int(trid_arg) 238 | except (ValueError, TypeError): 239 | return "Invalid ClientTransactionID", 400 240 | 241 | if (clid < 0): 242 | return "Invalid ClientID", 400 243 | if (trid < 0): 244 | return "Invalid ClientTransactionID", 400 245 | 246 | return AlpacaServer.callMethod(devtype, devnr, method, request) 247 | 248 | # root page, redirect to server setup page 249 | @alpaca_app.route('/') 250 | async def index(request): 251 | return redirect('/setup') 252 | 253 | # device setup page 254 | @alpaca_app.route('/setup/v1///setup', methods=['GET', 'PUT']) 255 | async def devsetup(request,devtype,devnr): 256 | return AlpacaServer.devices[devtype][devnr].setupRequest(request) 257 | 258 | # return server API versions 259 | @alpaca_app.get('/management/apiversions') 260 | async def get_mgmt_apiversions(request): 261 | return AlpacaServer.reply(request, AlpacaServer.getServerApiVersions(), mngmnt_api=True) 262 | 263 | # return server description 264 | @alpaca_app.get('/management/v1/description') 265 | async def get_mgmt_description(request): 266 | return AlpacaServer.reply(request, AlpacaServer.getServerDescr(), mngmnt_api=True) 267 | 268 | # return configured devices 269 | @alpaca_app.get('/management/v1/configureddevices') 270 | async def get_mgmt_configureddevices(request): 271 | return AlpacaServer.reply(request, AlpacaServer.getConfDevices(), mngmnt_api=True) 272 | 273 | # server setup page 274 | @alpaca_app.route('/setup', methods=['GET', 'POST']) 275 | async def setup(req): 276 | if req.method == 'POST': # apply new settings on POST 277 | AlpacaServer.config["serverPort"] = req.form.get('srvport') 278 | AlpacaServer.config["discoveryPort"] = req.form.get('discport') 279 | writeJson("servercfg.json", AlpacaServer.config) 280 | # render server setup page 281 | return render_template('mipysetup.html', title="RasPi Pico Alpaca Server Setup", tab = AlpacaServer.getConfDevices(), srvcfg = AlpacaServer.config) 282 | --------------------------------------------------------------------------------