add CircuitPython library sources

This commit is contained in:
James Devine 2020-10-06 11:53:44 +01:00
Родитель addeaeb3bb
Коммит f4c26113ff
6 изменённых файлов: 504 добавлений и 0 удалений

Просмотреть файл

@ -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

Двоичные данные
images/circuitpython-drive.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 736 KiB

376
jacdac.py Normal file
Просмотреть файл

@ -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)

9
samples/process/code.py Normal file
Просмотреть файл

@ -0,0 +1,9 @@
from jacdac import JDStack
import board
import time
jd = JDStack(board.P12)
while True:
jd.process()
time.sleep(.01)