add CircuitPython library sources
This commit is contained in:
Родитель
addeaeb3bb
Коммит
f4c26113ff
63
README.md
63
README.md
|
@ -1,3 +1,66 @@
|
|||
# JACDAC CircuitPython
|
||||
|
||||
This repository implements JACDAC in CircuitPython. Read more about JACDAC at [jacdac.org](https://jacdac.org).
|
||||
|
||||
## Usage
|
||||
|
||||
```py
|
||||
from jacdac import JDStack
|
||||
import board
|
||||
import time
|
||||
|
||||
jd = JDStack(board.P12)
|
||||
|
||||
while True:
|
||||
# other app code
|
||||
jd.process()
|
||||
time.sleep(.1)
|
||||
```
|
||||
|
||||
Above is the most minimal program to operate the JACDAC stack. `process()` must be called regularly to process received packets and to regularly send advertisements from your device. In the sample above, `process()` is called every 100 milliseconds, though the time between calls to process is tuned depending on bus activity and application demands.
|
||||
|
||||
For example, if your program is hosting the accelerometer service, `process()` will need to be called at least every 10 milliseconds. The accelerometer service may be configured to stream packets every 10 milliseconds, which defines the minimum `process()` call speed:
|
||||
|
||||
```py
|
||||
from jacdac import JDStack, JDAccelerometer
|
||||
import time
|
||||
import adafruit_lsm6ds.lsm6ds33
|
||||
import board
|
||||
|
||||
jd = JDStack(board.P12)
|
||||
i2c = board.I2C()
|
||||
accelerometer = adafruit_lsm6ds.lsm6ds33.LSM6DS33(i2c)
|
||||
jd_accel = JDAccelerometer(accelerometer, jd)
|
||||
jd.add_service(jd_accel)
|
||||
|
||||
|
||||
while True:
|
||||
# other app code
|
||||
jd.process()
|
||||
time.sleep(.01)
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
Currently, JACDAC is not merged into mainline CircuitPython and only works on NRF52-based boards. A JACDAC-enabled uf2 CircuitPython file for the Adafruit Clue is provided in the samples folder. You will have to compile CircuitPython for other NRF52-based boards.
|
||||
|
||||
To build a JACDAC CircuitPython binary:
|
||||
|
||||
1. clone https://github.com/jamesadevine/circuitpython and checkout the `jacdac` branch.
|
||||
2. `git submodule init && git submodule sync && git submodule update`
|
||||
3. `cd ports/nrf`
|
||||
4. `make BOARD=<your board> -j 10`. For instance to build for clue the command would be: `make BOARD=clue_nrf52840_express -j 10`. The list of boards can be found in `ports/nrf/boards`
|
||||
|
||||
To use the JACDAC stack in your CircuitPython program simply copy jacdac.py to the `libs` folder on the CIRCUITPY drive. For some of the samples contained in this repository, additional libraries are also required:
|
||||
|
||||
* `adafruit_bus_device`
|
||||
* `adafruit_lsm6ds`
|
||||
* `adafruit_display_text`
|
||||
* `adafruit_register`
|
||||
|
||||
The latest versions of these libraries can be obtained from the [Adafruit CircuitPython Bundle](https://github.com/adafruit/Adafruit_CircuitPython_Bundle/). After installation the CIRCUITPY drive should look like:
|
||||
|
||||
![circuitpython flash drive layout](./images/circuitpython-drive.png)
|
||||
|
||||
# Contributing
|
||||
|
||||
|
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 736 KiB |
|
@ -0,0 +1,376 @@
|
|||
from busio import JACDAC
|
||||
import time
|
||||
import struct
|
||||
import microcontroller
|
||||
import supervisor
|
||||
|
||||
JD_FRAME_HEADER_SIZE = 12
|
||||
JD_FRAME_FLAG_COMMAND = 0x01
|
||||
JD_FRAME_FLAG_ACK_REQUESTED = 0x02
|
||||
JD_FRAME_FLAG_IDENTIFIER_IS_SERVICE_CLASS = 0x04
|
||||
|
||||
JD_PACKET_HEADER_SIZE = 4
|
||||
REG_INTENSITY = 0x01
|
||||
|
||||
REG_VALUE = 0x02
|
||||
REG_STREAMING_SAMPLES = 0x03
|
||||
REG_STREAMING_INTERVAL = 0x04
|
||||
REG_LOW_THRESHOLD = 0x05
|
||||
REG_HIGH_THRESHOLD = 0x06
|
||||
REG_MAX_POWER = 0x07
|
||||
REG_READING = 0x101
|
||||
|
||||
CMD_GET_REG = 0x1000
|
||||
CMD_SET_REG = 0x2000
|
||||
CMD_TYPE_MASK = 0xf000
|
||||
CMD_VAL_MASK = 0x0fff
|
||||
|
||||
CMD_ADVERTISEMENT_DATA = 0x00
|
||||
CMD_EVENT = 0x01
|
||||
CMD_CALIBRATE = 0x02
|
||||
CMD_GET_DESCRIPTION = 0x03
|
||||
CMD_CTRL_NOOP = 0x80
|
||||
CMD_CTRL_IDENTIFY = 0x81
|
||||
CMD_CTRL_RESET = 0x82
|
||||
|
||||
REG_CTRL_DEVICE_DESCRIPTION = 0x180
|
||||
REG_CTRL_DEVICE_CLASS = 0x181
|
||||
REG_CTRL_TEMPERATURE = 0x182
|
||||
REG_CTRL_FIRMWARE_VERSION = 0x185
|
||||
REG_CTRL_MICROS_SINCE_BOOT = 0x186
|
||||
|
||||
jacdac_instance = None
|
||||
|
||||
class JDHeader:
|
||||
data = None
|
||||
crc = 0
|
||||
size = 0
|
||||
flags = 0
|
||||
device_id = 0
|
||||
|
||||
def __init__(self, buf):
|
||||
self.data = buf
|
||||
if self.data is not None:
|
||||
self.crc, self.size, self.flags, self.device_id = struct.unpack_from('<HBBQ', self.data, 0)
|
||||
|
||||
def serialize(self):
|
||||
b = bytearray(12)
|
||||
struct.pack_into('<HBBQ', b, 0, 0, self.size, self.flags, self.device_id) # blank crc, computed by phys
|
||||
return b
|
||||
|
||||
|
||||
class JDPacket:
|
||||
header = None
|
||||
data = None
|
||||
size = None
|
||||
service_instance = None
|
||||
service_command = None
|
||||
|
||||
def __init__(self, header, data):
|
||||
self.header = header
|
||||
|
||||
if self.header is None:
|
||||
self.header = JDHeader(None)
|
||||
|
||||
if data is not None:
|
||||
upack = struct.unpack_from('<BBH', data, 0)
|
||||
self.size = upack[0]
|
||||
self.service_instance = upack[1]
|
||||
self.service_command = upack[2]
|
||||
self.data = data[4:]
|
||||
|
||||
def is_reg_set(self):
|
||||
return (self.service_command >> 12) == (CMD_SET_REG >> 12)
|
||||
|
||||
def is_reg_get(self):
|
||||
return (self.service_command >> 12) == (CMD_GET_REG >> 12)
|
||||
|
||||
def is_command(self):
|
||||
if self.data is None:
|
||||
return False
|
||||
|
||||
return not (self.header.flags & JD_FRAME_FLAG_COMMAND) == 0
|
||||
|
||||
def is_register(self):
|
||||
if self.data is None:
|
||||
return False
|
||||
|
||||
return (self.header.flags & JD_FRAME_FLAG_COMMAND) == 0
|
||||
|
||||
def serialize(self):
|
||||
b = bytearray(4)
|
||||
struct.pack_into('<BBH', b, 0, len(self.data), self.service_instance, self.service_command)
|
||||
return self.header.serialize() + b + self.data
|
||||
|
||||
|
||||
class JDServiceHost:
|
||||
service_instance = None
|
||||
service_class = None
|
||||
time = 0
|
||||
stack = None
|
||||
def __init__(self, service_class, stack):
|
||||
self.service_class = service_class
|
||||
self.time = time.monotonic()
|
||||
self.stack = stack
|
||||
|
||||
def handle_packet(self, p):
|
||||
if p.is_command():
|
||||
self.handle_command(p)
|
||||
else:
|
||||
self.handle_report(p)
|
||||
|
||||
def handle_report(self, p):
|
||||
return
|
||||
|
||||
def handle_command(self, p):
|
||||
if p.is_reg_set():
|
||||
self.handle_register_set(p)
|
||||
else:
|
||||
self.handle_register_get(p)
|
||||
|
||||
def handle_register_set(self, p):
|
||||
return
|
||||
|
||||
def handle_register_get(self, p):
|
||||
return
|
||||
|
||||
def send_report(self, cmd, data):
|
||||
resp = JDPacket(None, None)
|
||||
resp.service_command = cmd
|
||||
resp.service_instance = self.service_instance
|
||||
resp.data = data
|
||||
self.stack.send_report(resp)
|
||||
|
||||
def tick(self, now):
|
||||
return
|
||||
|
||||
|
||||
class JDControl(JDServiceHost):
|
||||
stack = None
|
||||
def __init__(self, stack):
|
||||
super().__init__(0, stack) # control is service class 0
|
||||
|
||||
def handle_command(self, p):
|
||||
cmd = p.service_command & CMD_VAL_MASK
|
||||
resp = JDPacket(None, None)
|
||||
resp.service_instance = 0
|
||||
|
||||
if cmd == CMD_CTRL_RESET:
|
||||
supervisor.reload()
|
||||
elif cmd == REG_CTRL_DEVICE_DESCRIPTION:
|
||||
resp.service_command = CMD_GET_REG | REG_CTRL_DEVICE_DESCRIPTION
|
||||
resp.data = bytearray("Generic CircuitPython device")
|
||||
self.stack.send(resp)
|
||||
elif cmd == REG_CTRL_DEVICE_CLASS:
|
||||
resp.service_command = CMD_GET_REG | REG_CTRL_DEVICE_CLASS
|
||||
resp.data = bytearray(4)
|
||||
struct.pack_into("<I", resp.data, 0, 0xAAAAAAAA)
|
||||
self.stack.send(resp)
|
||||
elif cmd == REG_CTRL_FIRMWARE_VERSION:
|
||||
resp.service_command = CMD_GET_REG | REG_CTRL_FIRMWARE_VERSION
|
||||
resp.data = bytearray("v0.0.1")
|
||||
self.stack.send(resp)
|
||||
|
||||
def tick(self, now):
|
||||
if now - self.time < .5:
|
||||
return
|
||||
|
||||
self.time = now
|
||||
|
||||
adv = JDPacket(None, None)
|
||||
adv.service_instance = 0
|
||||
adv.service_command = 0
|
||||
adv.data = bytearray(len(self.stack.services) * 4)
|
||||
|
||||
i = 0
|
||||
for s in self.stack.services:
|
||||
struct.pack_into('<I', adv.data, i, s.service_class)
|
||||
i += 4
|
||||
|
||||
self.stack.send(adv)
|
||||
|
||||
class JDDevice:
|
||||
last_packet = None
|
||||
ticks = 5
|
||||
service_classes = None
|
||||
device_id = None
|
||||
short_id = None
|
||||
time = 0
|
||||
|
||||
def __init__(self, packet, short_id):
|
||||
if packet is not None:
|
||||
self.device_id = packet.header.device_id
|
||||
self.short_id = short_id
|
||||
self.update(packet)
|
||||
self.time = time.monotonic()
|
||||
|
||||
def update(self, packet):
|
||||
self.ticks = 5
|
||||
self.service_classes = struct.unpack_from('<' + ("I" * int(packet.size/4)), packet.data, 0)
|
||||
|
||||
def tick(self, t):
|
||||
if self.time == 0:
|
||||
return False
|
||||
|
||||
if t - self.time >= .5:
|
||||
self.ticks -= 1
|
||||
|
||||
return self.ticks == 0
|
||||
|
||||
class JDSensor(JDServiceHost):
|
||||
streaming = -1
|
||||
streaming_interval = .1
|
||||
|
||||
def __init__(self, service_class, stack):
|
||||
super().__init__(service_class, stack)
|
||||
|
||||
def tick(self, now):
|
||||
if self.streaming >= 0 and (now - self.time) >= self.streaming_interval:
|
||||
self.time = now
|
||||
self.streaming -= 1
|
||||
self.__sensor_report()
|
||||
|
||||
def handle_register_set(self, p):
|
||||
cmd = p.service_command & CMD_VAL_MASK
|
||||
if cmd == REG_STREAMING_SAMPLES:
|
||||
self.streaming = struct.unpack_from('<B',p.data, 0)[0]
|
||||
if cmd == REG_STREAMING_INTERVAL:
|
||||
self.streaming_interval = float(struct.unpack_from('<I',p.data, 0)[0]) / 1000
|
||||
|
||||
def handle_register_get(self, p):
|
||||
cmd = p.service_command & CMD_VAL_MASK
|
||||
if cmd == REG_STREAMING_SAMPLES:
|
||||
b = bytearray(1)
|
||||
val = 0
|
||||
if self.streaming > 0:
|
||||
val = self.streaming
|
||||
struct.pack_into('<B', b, 0, val)
|
||||
self.send_report(CMD_GET_REG | REG_STREAMING_SAMPLES, b)
|
||||
|
||||
if cmd == REG_STREAMING_INTERVAL:
|
||||
resp = JDPacket(None, None)
|
||||
b = bytearray(4)
|
||||
struct.pack_into('<I', b, 0, int(self.streaming_interval * 1000))
|
||||
self.send_report(CMD_GET_REG | REG_STREAMING_INTERVAL, b)
|
||||
|
||||
|
||||
class JDAccelerometer(JDSensor):
|
||||
accelerometer = None
|
||||
def __init__(self, accelerometer, stack):
|
||||
super().__init__(0x1f140409, stack)
|
||||
self.accelerometer = accelerometer
|
||||
|
||||
def __sensor_report(self):
|
||||
p = JDPacket(None, None)
|
||||
b = bytearray(6) # 3 sixteen bit samples
|
||||
raw = self.accelerometer._raw_accel_data
|
||||
struct.pack_into('<hhh', b, 0, raw[0], raw[1], raw[2])
|
||||
self.send_report(CMD_GET_REG | REG_READING, b)
|
||||
|
||||
def handle_register_get(self, p):
|
||||
super().handle_register_get(p)
|
||||
cmd = p.service_command & CMD_VAL_MASK
|
||||
if cmd == 0x101:
|
||||
self.__sensor_report()
|
||||
|
||||
class JDStack:
|
||||
bus = None
|
||||
dev = None
|
||||
ctrl = None
|
||||
buf = bytearray(256)
|
||||
services = []
|
||||
devices = []
|
||||
udid = 0
|
||||
|
||||
def __init__(self, pin):
|
||||
jacdac_instance = self
|
||||
self.bus = JACDAC(pin)
|
||||
self.ctrl = JDControl(self)
|
||||
self.services = [self.ctrl]
|
||||
|
||||
self.dev = JDDevice(None, None)
|
||||
self.dev.device_id = struct.unpack_from('<Q',microcontroller.cpu.uid,0)[0]
|
||||
self.dev.short_id = self.bus.hash(microcontroller.cpu.uid).decode()
|
||||
self.dev.ticks = -1
|
||||
|
||||
self.devices += [self.dev]
|
||||
|
||||
####
|
||||
# send a packet without any modifications by the stack
|
||||
####
|
||||
def send(self, packet, device_id = None):
|
||||
if device_id:
|
||||
packet.header.device_id = device_id
|
||||
else:
|
||||
packet.header.device_id = self.dev.device_id
|
||||
packet.header.size = len(packet.data) + JD_PACKET_HEADER_SIZE
|
||||
self.bus.send(packet.serialize())
|
||||
|
||||
####
|
||||
# send a command packet
|
||||
####
|
||||
def send_command(self, packet, device_id = None):
|
||||
if device_id:
|
||||
packet.header.device_id = device_id
|
||||
else:
|
||||
packet.header.device_id = self.dev.device_id
|
||||
packet.header.flags |= JD_FRAME_FLAG_COMMAND
|
||||
packet.header.size = len(packet.data) + JD_PACKET_HEADER_SIZE
|
||||
self.bus.send(packet.serialize())
|
||||
|
||||
####
|
||||
# send a report packet
|
||||
####
|
||||
def send_report(self, packet, device_id = None):
|
||||
if device_id:
|
||||
packet.header.device_id = device_id
|
||||
else:
|
||||
packet.header.device_id = self.dev.device_id
|
||||
packet.header.size = len(packet.data) + JD_PACKET_HEADER_SIZE
|
||||
self.bus.send(packet.serialize())
|
||||
|
||||
def add_service(self, service):
|
||||
#todo: in future we may want to have differentiation between client/host services
|
||||
self.services += [service]
|
||||
service.service_instance = len(self.services) - 1
|
||||
|
||||
def process(self):
|
||||
p = self.bus.receive(self.buf)
|
||||
|
||||
packets = []
|
||||
|
||||
while p:
|
||||
header = JDHeader(self.buf[:JD_FRAME_HEADER_SIZE])
|
||||
i = JD_FRAME_HEADER_SIZE
|
||||
|
||||
while i < JD_FRAME_HEADER_SIZE + header.size:
|
||||
|
||||
size = struct.unpack_from('B', self.buf, i)[0]
|
||||
packets += [JDPacket(header, bytearray(self.buf[i: i + size + JD_PACKET_HEADER_SIZE]))]
|
||||
i += size + JD_PACKET_HEADER_SIZE
|
||||
|
||||
p = self.bus.receive(self.buf)
|
||||
|
||||
for p in packets:
|
||||
if p.header.device_id == self.dev.device_id and p.service_instance < len(self.services):
|
||||
self.services[p.service_instance].handle_packet(p)
|
||||
elif p.service_instance == 0 and p.service_command == CMD_ADVERTISEMENT_DATA:
|
||||
found = None
|
||||
for d in self.devices:
|
||||
if d.device_id == p.header.device_id:
|
||||
found = d
|
||||
break
|
||||
if found is None:
|
||||
self.devices += [JDDevice(p, self.bus.hash(p.header.data[4:12]).decode())]
|
||||
else:
|
||||
found.update(p)
|
||||
|
||||
now = time.monotonic()
|
||||
|
||||
for s in self.services:
|
||||
s.tick(now)
|
||||
|
||||
for d in self.devices:
|
||||
if d.tick(now):
|
||||
self.devices.remove(d)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
from jacdac import JDStack, JDAccelerometer
|
||||
import time
|
||||
import adafruit_lsm6ds.lsm6ds33
|
||||
import board
|
||||
|
||||
jd = JDStack(board.P12)
|
||||
i2c = board.I2C()
|
||||
accelerometer = adafruit_lsm6ds.lsm6ds33.LSM6DS33(i2c)
|
||||
jd_accel = JDAccelerometer(accelerometer, jd)
|
||||
jd.add_service(jd_accel)
|
||||
|
||||
|
||||
while True:
|
||||
jd.process()
|
||||
time.sleep(.01)
|
|
@ -0,0 +1,41 @@
|
|||
from jacdac import JDStack
|
||||
import board
|
||||
import displayio
|
||||
import time
|
||||
import terminalio
|
||||
from adafruit_display_text.label import Label
|
||||
|
||||
disp = board.DISPLAY
|
||||
jd = JDStack(board.P12)
|
||||
|
||||
known_services = {
|
||||
0x1d90e1c5:"aggregator",
|
||||
0x1f140409:"accelerometer"
|
||||
}
|
||||
|
||||
while True:
|
||||
dlist = displayio.Group()
|
||||
|
||||
row = 1
|
||||
for device in jd.devices:
|
||||
sid = str(device.short_id)
|
||||
if idx == 0:
|
||||
sid += " <self>"
|
||||
|
||||
dlabel = Label(terminalio.FONT, text=sid, color=0x0000FF)
|
||||
dlabel.x = 10
|
||||
dlabel.y = row * 10
|
||||
dlist.append(dlabel)
|
||||
|
||||
if idx > 0:
|
||||
sinfo = Label(terminalio.FONT, text=[known_services[s] for s in device.service_classes[1:] if s in known_services.keys()], color=0x0000FF)
|
||||
dlabel.x = 10
|
||||
dlabel.y = (row + 1) * 20
|
||||
dlist.append(sinfo)
|
||||
|
||||
row += 2
|
||||
|
||||
disp.show(dlist)
|
||||
|
||||
jd.process()
|
||||
time.sleep(.01)
|
|
@ -0,0 +1,9 @@
|
|||
from jacdac import JDStack
|
||||
import board
|
||||
import time
|
||||
|
||||
jd = JDStack(board.P12)
|
||||
|
||||
while True:
|
||||
jd.process()
|
||||
time.sleep(.01)
|
Загрузка…
Ссылка в новой задаче