├── 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 | | Device Type |
109 | Device Number |
110 | Description |
111 |
112 |
113 |
114 | {% for device in tab %}
115 |
116 | | {{ device["DeviceType"] }} |
117 | {{ device["DeviceNumber"] }} |
118 | {{ device["DeviceName"] }} |
119 |
120 | {% endfor %}
121 |
122 |
123 |
124 |
125 |
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 | 
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 | 
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 | 
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 | 
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 | 
198 |
199 |
200 |
201 | This is a sample breadboard setup for this example:
202 |
203 | 
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 |
--------------------------------------------------------------------------------