From e1bcf07614b4185e460274b8bbf31df86848d3cb Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Fri, 24 Mar 2023 09:12:04 -0500 Subject: [PATCH] add new project --- .../code.py | 204 ++++++++++++++++++ .../helvR08.pcf | Bin 0 -> 60344 bytes 2 files changed, 204 insertions(+) create mode 100644 CircuitPython_GetSuperpower_PicoW_OpenAI/code.py create mode 100644 CircuitPython_GetSuperpower_PicoW_OpenAI/helvR08.pcf diff --git a/CircuitPython_GetSuperpower_PicoW_OpenAI/code.py b/CircuitPython_GetSuperpower_PicoW_OpenAI/code.py new file mode 100644 index 000000000..9c2db4174 --- /dev/null +++ b/CircuitPython_GetSuperpower_PicoW_OpenAI/code.py @@ -0,0 +1,204 @@ +# SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries +# SPDX-License-Identifier: MIT +import json +import os +import ssl + +import board +import displayio +import digitalio +import keypad +import socketpool +from wifi import radio + +import adafruit_requests +import adafruit_displayio_ssd1306 +from adafruit_bitmap_font.bitmap_font import load_font +from adafruit_display_text import wrap_text_to_pixels +from adafruit_display_text.bitmap_label import Label +from adafruit_ticks import ticks_add, ticks_less, ticks_ms + + +# Choose your own prompt and wait messages, either by changing it below inside +# the """triple quoted""" string, or by putting it in your settings.toml file, +# like so: +# +# MY_PROMPT="Give me an idea for a plant-based dinner. Write one sentence" +# PLEASE_WAIT="Cooking something up just for you" + +# Here are some prompts you might want to try: + +# Give me an idea for a plant-based dinner. Write one sentence +# +# Give jepler a description as a comic book supervillain. write one sentence. +# +# Invent and describe an alien species. write one sentence +# +# Invent a zany "as seen on TV product" that can't possibly work. One sentence +# +# Tell a 1-sentence story about a kitten and a funny mishap +# +# Make up a 1-sentence fortune for me +# +# In first person, write a 1-sentence story about an AI avoiding boredom in a creative way. +# +# Pick an everyday object (don't say what it is) and describe it using only the +# ten hundred most common words. +# +# Invent an alien animal or plant, name it, and vividly describe it in 1 +# sentence +prompt=os.getenv("MY_PROMPT", """ +Write 1 setence starting "you can" about an unconventional but useful superpower +""").strip() +please_wait=os.getenv("PLEASE_WAIT", """ +Finding superpower +""").strip() + +openai_api_key = os.getenv("OPENAI_API_KEY") + +nice_font = load_font("helvR08.pcf") +line_spacing = 0.68 + +# i2c display setup +displayio.release_displays() +oled_reset = board.GP9 + +# STEMMA I2C on picowbell +i2c = board.STEMMA_I2C() +display_bus = displayio.I2CDisplay(i2c, device_address=0x3D, reset=oled_reset) + +WIDTH = 128 +HEIGHT = 64 +offset_y = 5 + +display = adafruit_displayio_ssd1306.SSD1306( + display_bus, width=WIDTH, height=HEIGHT +) +if openai_api_key is None: + input("Place your\nOPENAI_API_KEY\nin settings.toml") +display.auto_refresh = False +main_group = displayio.Group() +display.root_group = main_group + +terminal = Label( + font=nice_font, + color=0xFFFFFF, + background_color=0, + line_spacing=line_spacing, + anchor_point=(0, 0), + anchored_position=(0, 0), +) +max_lines = display.height // int(nice_font.get_bounding_box()[1] * terminal.line_spacing) +main_group.append(terminal) + +class WrappedTextDisplay: + def __init__(self): + self.line_offset = 0 + self.lines = [] + self.text = "" + + def add_text(self, new_text): + self.set_text(self.text + new_text) + self.scroll_to_end() + + def set_text(self, text): + print("\033[H\033[2J", end=text) + self.text = text + self.lines = wrap_text_to_pixels(text, display.width, nice_font) + self.line_offset = 0 + + def show(self, text): + self.set_text(text) + self.refresh() + + def add_show(self, new_text): + self.add_text(new_text) + self.refresh() + + def scroll_to_end(self): + self.line_offset = self.max_offset() + + def scroll_next_line(self): + max_offset = self.max_offset() + if max_offset > 0: + line_offset = self.line_offset + 1 + self.line_offset = line_offset % (max_offset + 1) + + def max_offset(self): + return max(0, len(self.lines) - max_lines) + + def on_last_line(self): + return self.line_offset == self.max_offset() + + def refresh(self): + text = '\n'.join(self.lines[self.line_offset : self.line_offset + max_lines]) + terminal.text = text + display.refresh() +wrapped_text = WrappedTextDisplay() + +def wait_button_scroll_text(): + led.switch_to_output(True) + deadline = ticks_add(ticks_ms(), + 5000 if wrapped_text.on_last_line() else 1000) + while True: + if (event := keys.events.get()) and event.pressed: + break + if wrapped_text.max_offset() > 0 and ticks_less(deadline, ticks_ms()): + wrapped_text.scroll_next_line() + wrapped_text.refresh() + deadline = ticks_add(deadline, + 5000 if wrapped_text.on_last_line() else 1000) + led.value = False + +if radio.ipv4_address is None: + wrapped_text.show(f"connecting to {os.getenv('WIFI_SSID')}") + radio.connect(os.getenv('WIFI_SSID'), os.getenv('WIFI_PASSWORD')) +requests = adafruit_requests.Session(socketpool.SocketPool(radio), ssl.create_default_context()) + +def iter_lines(resp): + partial_line = [] + for c in resp.iter_content(): + if c == b'\n': + yield (b"".join(partial_line)).decode('utf-8') + del partial_line[:] + else: + partial_line.append(c) + if partial_line: + yield (b"".join(partial_line)).decode('utf-8') + +full_prompt = [ + {"role": "user", "content": prompt}, +] + +keys = keypad.Keys((board.GP14,), value_when_pressed=False) +led = digitalio.DigitalInOut(board.LED) + +while True: + wrapped_text.show(please_wait) + + with requests.post("https://api.openai.com/v1/chat/completions", + json={"model": "gpt-3.5-turbo", "messages": full_prompt, "stream": True}, + headers={ + "Authorization": f"Bearer {openai_api_key}", + }, + ) as response: + + wrapped_text.set_text("") + if response.status_code != 200: + wrapped_text.show(f"Uh oh! {response.status_code}: {response.reason}") + else: + wrapped_text.show("") + for line in iter_lines(response): + # print(line) + # continue + if line.startswith("data: [DONE]"): + break + if line.startswith("data:"): + content = json.loads(line[5:]) + try: + token = content['choices'][0]['delta'].get('content', '') + except (KeyError, IndexError) as e: + token = None + if token: + wrapped_text.add_show(token) + wait_button_scroll_text() diff --git a/CircuitPython_GetSuperpower_PicoW_OpenAI/helvR08.pcf b/CircuitPython_GetSuperpower_PicoW_OpenAI/helvR08.pcf new file mode 100644 index 0000000000000000000000000000000000000000..60d235b259f03fe0287a742dc5d613d1506ebc98 GIT binary patch literal 60344 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 Mc=Eq+%D)8uKd1E%D*ylh literal 0 HcmV?d00001