├── cosmic-paste ├── .gitignore ├── config-unicorns.example.json ├── README.md ├── cosmic-paste.py └── cosmic-paste.html ├── cosmic-emoji-react ├── src │ ├── react-app-env.d.ts │ ├── types │ │ └── paint.ts │ ├── setupTests.ts │ ├── widgets │ │ ├── Settings.css │ │ ├── Controls.css │ │ ├── Brightness.css │ │ ├── Settings.tsx │ │ ├── Preview.css │ │ ├── Brightness.tsx │ │ └── Preview.tsx │ ├── App.test.tsx │ ├── index.css │ ├── reportWebVitals.ts │ ├── index.tsx │ ├── config │ │ └── config.ts │ ├── App.css │ ├── useQueryParamState.ts │ ├── logo.svg │ └── App.tsx ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── config-unicorns.example.json │ ├── manifest.json │ └── index.html ├── craco.config.js ├── delete-extra-build-files.sh ├── .gitignore ├── tsconfig.json ├── package.json ├── pico │ └── cosmic-emoji.py └── README.md ├── cosmic-doomguy ├── doomguy.jpg ├── screenshot │ └── doomguy-photo-400.jpg ├── README.md └── doomguy.py └── README.md /cosmic-paste/.gitignore: -------------------------------------------------------------------------------- 1 | config-unicorns.json -------------------------------------------------------------------------------- /cosmic-emoji-react/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /cosmic-doomguy/doomguy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscareycode/cosmic-unicorn/HEAD/cosmic-doomguy/doomguy.jpg -------------------------------------------------------------------------------- /cosmic-emoji-react/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /cosmic-emoji-react/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscareycode/cosmic-unicorn/HEAD/cosmic-emoji-react/public/favicon.ico -------------------------------------------------------------------------------- /cosmic-emoji-react/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscareycode/cosmic-unicorn/HEAD/cosmic-emoji-react/public/logo192.png -------------------------------------------------------------------------------- /cosmic-emoji-react/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscareycode/cosmic-unicorn/HEAD/cosmic-emoji-react/public/logo512.png -------------------------------------------------------------------------------- /cosmic-emoji-react/public/config-unicorns.example.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Cosmic 1", 4 | "ip": "10.200.0.122", 5 | "type": "cosmic" 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /cosmic-doomguy/screenshot/doomguy-photo-400.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscareycode/cosmic-unicorn/HEAD/cosmic-doomguy/screenshot/doomguy-photo-400.jpg -------------------------------------------------------------------------------- /cosmic-emoji-react/craco.config.js: -------------------------------------------------------------------------------- 1 | // craco.config.js 2 | module.exports = { 3 | webpack: { 4 | alias: { 5 | "react": "preact/compat", 6 | "react-dom": "preact/compat" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /cosmic-emoji-react/src/types/paint.ts: -------------------------------------------------------------------------------- 1 | export interface UnicornType { 2 | name: string; 3 | ip: string; 4 | type: 'cosmic' | 'galactic'; 5 | 6 | dataUrl?: string | undefined; 7 | dataRgbaArray?: number[] | undefined; 8 | brightness?: number; 9 | 10 | } -------------------------------------------------------------------------------- /cosmic-emoji-react/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /cosmic-emoji-react/src/widgets/Settings.css: -------------------------------------------------------------------------------- 1 | .Settings { 2 | 3 | } 4 | 5 | .Settings .settings-open { 6 | position: absolute; 7 | top: 20px; 8 | right: 20px; 9 | bottom: 20px; 10 | left: 20px; 11 | 12 | background-color: #222; 13 | border: 2px solid #ccc; 14 | border-radius: 10px; 15 | z-index: 10; 16 | } -------------------------------------------------------------------------------- /cosmic-emoji-react/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /cosmic-emoji-react/delete-extra-build-files.sh: -------------------------------------------------------------------------------- 1 | rm pico/cosmic-emoji/asset-manifest.json 2 | rm pico/cosmic-emoji/favicon.ico 3 | rm pico/cosmic-emoji/logo*.png 4 | rm pico/cosmic-emoji/manifest.json 5 | rm pico/cosmic-emoji/robots.txt 6 | rm pico/cosmic-emoji/config-unicorns.json 7 | 8 | rm pico/cosmic-emoji/static/css/*.map 9 | rm pico/cosmic-emoji/static/js/*.map 10 | rm pico/cosmic-emoji/static/js/*.txt 11 | -------------------------------------------------------------------------------- /cosmic-paste/config-unicorns.example.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Cosmic 1", 4 | "ip": "10.200.0.122", 5 | "type": "cosmic" 6 | }, 7 | { 8 | "name": "Cosmic 2", 9 | "ip": "10.200.0.125", 10 | "type": "cosmic" 11 | }, 12 | { 13 | "name": "Cosmic 3", 14 | "ip": "10.200.0.127", 15 | "type": "cosmic" 16 | }, 17 | { 18 | "name": "Cosmic 4", 19 | "ip": "10.200.0.126", 20 | "type": "cosmic" 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /cosmic-emoji-react/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | 9 | background-color: #222; 10 | color: #fff; 11 | } 12 | 13 | code { 14 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 15 | monospace; 16 | } 17 | -------------------------------------------------------------------------------- /cosmic-emoji-react/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | /public/config-unicorns.json 4 | /pico/releases 5 | /pico/cosmic-emoji 6 | 7 | # dependencies 8 | /node_modules 9 | /.pnp 10 | .pnp.js 11 | 12 | # testing 13 | /coverage 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | .env.local 21 | .env.development.local 22 | .env.test.local 23 | .env.production.local 24 | 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | -------------------------------------------------------------------------------- /cosmic-emoji-react/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /cosmic-emoji-react/src/widgets/Controls.css: -------------------------------------------------------------------------------- 1 | .Controls { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | margin: 8px 0; 6 | user-select: none; 7 | } 8 | 9 | .Controls > div { 10 | background-color: rgba(0, 0, 0, 0.5); 11 | border-radius: 5px; 12 | margin-left: auto; 13 | margin-right: auto; 14 | font-size: 0.7em; 15 | padding: 1px 5px; 16 | 17 | display: flex; 18 | align-items: center; 19 | } 20 | 21 | .Controls > div > select { 22 | background-color: rgba(0, 0, 0, 0.5); 23 | color: white; 24 | } -------------------------------------------------------------------------------- /cosmic-emoji-react/src/widgets/Brightness.css: -------------------------------------------------------------------------------- 1 | .Brightness { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | margin: 8px 0; 6 | user-select: none; 7 | } 8 | 9 | .Brightness > div { 10 | background-color: rgba(0, 0, 0, 0.5); 11 | border-radius: 5px; 12 | margin-left: auto; 13 | margin-right: auto; 14 | font-size: 0.7em; 15 | padding: 1px 5px; 16 | 17 | display: flex; 18 | align-items: center; 19 | } 20 | 21 | .Brightness > div > select { 22 | background-color: rgba(0, 0, 0, 0.5); 23 | color: white; 24 | } -------------------------------------------------------------------------------- /cosmic-emoji-react/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Cosmic Emoji", 3 | "name": "Cosmic Emoji", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /cosmic-emoji-react/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot( 8 | document.getElementById('root') as HTMLElement 9 | ); 10 | root.render( 11 | 12 | 13 | 14 | ); 15 | 16 | // If you want to start measuring performance in your app, pass a function 17 | // to log results (for example: reportWebVitals(console.log)) 18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 19 | reportWebVitals(); 20 | -------------------------------------------------------------------------------- /cosmic-emoji-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /cosmic-emoji-react/src/config/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is no longer being used. Moved config over to config-me.json 3 | * which is pulled in at runtime and not part of the bundle 4 | */ 5 | 6 | import { UnicornType } from '../types/paint'; 7 | 8 | export const defaultUnicornConfigs: UnicornType[] = [ 9 | // { 10 | // name: 'Galactic', 11 | // ip: '10.200.0.123', 12 | // type: 'galactic', 13 | // dataUrl: undefined, 14 | // }, 15 | { 16 | name: 'Cosmic 1', 17 | ip: '10.200.0.122', 18 | type: 'cosmic', 19 | dataUrl: undefined, 20 | dataRgbaArray: undefined, 21 | }, 22 | { 23 | name: 'Cosmic 2', 24 | ip: '10.200.0.125', 25 | type: 'cosmic', 26 | dataUrl: undefined, 27 | dataRgbaArray: undefined, 28 | }, 29 | { 30 | name: 'Cosmic 3', 31 | ip: '10.200.0.126', 32 | type: 'cosmic', 33 | dataUrl: undefined, 34 | dataRgbaArray: undefined, 35 | }, 36 | { 37 | name: 'Office', 38 | ip: '10.200.0.127', 39 | type: 'cosmic', 40 | dataUrl: undefined, 41 | dataRgbaArray: undefined, 42 | }, 43 | ]; -------------------------------------------------------------------------------- /cosmic-emoji-react/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | 4 | } 5 | 6 | .App-logo { 7 | height: 40vmin; 8 | pointer-events: none; 9 | } 10 | 11 | @media (prefers-reduced-motion: no-preference) { 12 | .App-logo { 13 | animation: App-logo-spin infinite 20s linear; 14 | } 15 | } 16 | 17 | .App-header { 18 | background-color: #282c34; 19 | min-height: 100vh; 20 | display: flex; 21 | flex-direction: column; 22 | align-items: center; 23 | justify-content: center; 24 | font-size: calc(10px + 2vmin); 25 | color: white; 26 | } 27 | 28 | .App-link { 29 | color: #61dafb; 30 | } 31 | 32 | @keyframes App-logo-spin { 33 | from { 34 | transform: rotate(0deg); 35 | } 36 | to { 37 | transform: rotate(360deg); 38 | } 39 | } 40 | 41 | 42 | .canvas-area { 43 | position: absolute; 44 | bottom: 10px; 45 | right: 20px; 46 | z-index: 2; 47 | opacity: 0.3; 48 | } 49 | .canvas-area canvas { 50 | border: 1px solid orange; 51 | } 52 | 53 | 54 | .fetch-error-message { 55 | border: 1px solid darkred; 56 | background-color: darkred; 57 | color: white; 58 | margin: 10px 10px 2px 10px; 59 | border-radius: 7px; 60 | } -------------------------------------------------------------------------------- /cosmic-emoji-react/src/widgets/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | // import { h, render } from 'preact'; 3 | // import { useEffect, useState } from 'preact/hooks'; 4 | 5 | import { UnicornType } from "../types/paint"; 6 | 7 | import './Settings.css'; 8 | 9 | interface SettingsProps { 10 | unicornConfigs: UnicornType[]; 11 | setUnicornConfigs: React.Dispatch>; 12 | } 13 | 14 | const Settings = ({ 15 | unicornConfigs, 16 | setUnicornConfigs, 17 | }: SettingsProps) => { 18 | 19 | const [isOpen, setIsOpen] = useState(false); 20 | 21 | const loop = unicornConfigs.map((uc, i) => { 22 | return ( 23 |
24 | 25 | {uc.name}
26 | {uc.ip}
27 | {uc.type}
28 |
29 |
30 | ); 31 | }); 32 | 33 | return ( 34 |
35 | Settings 36 |
37 | 38 | {isOpen && ( 39 |
40 | Settings 41 | 42 |
43 |
44 | {loop} 45 |
46 | )} 47 |
48 | ); 49 | }; 50 | 51 | export default Settings; -------------------------------------------------------------------------------- /cosmic-emoji-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cosmic-emoji-react", 3 | "version": "0.0.4", 4 | "private": true, 5 | "dependencies": { 6 | "emoji-picker-react": "^4.4.8", 7 | "lodash": "^4.17.21", 8 | "preact": "^10.13.2", 9 | "react": "^18.2.0", 10 | "react-dom": "^18.2.0", 11 | "react-idle-timer": "^5.5.3" 12 | }, 13 | "devDependencies": { 14 | "@craco/craco": "^7.1.0", 15 | "@testing-library/jest-dom": "^5.16.5", 16 | "@testing-library/react": "^13.4.0", 17 | "@testing-library/user-event": "^13.5.0", 18 | "@types/jest": "^27.5.2", 19 | "@types/lodash": "^4.14.194", 20 | "@types/node": "^16.18.20", 21 | "@types/react": "^18.0.29", 22 | "@types/react-dom": "^18.0.11", 23 | "react-scripts": "5.0.1", 24 | "typescript": "^4.9.5", 25 | "web-vitals": "^2.1.4" 26 | }, 27 | "scripts": { 28 | "start": "BROWSER=none PORT=3069 craco start", 29 | "build": "BUILD_PATH='./pico/cosmic-emoji' craco build", 30 | "test": "creaco test", 31 | "eject": "react-scripts eject" 32 | }, 33 | "eslintConfig": { 34 | "extends": [ 35 | "react-app", 36 | "react-app/jest" 37 | ] 38 | }, 39 | "browserslist": { 40 | "production": [ 41 | ">0.2%", 42 | "not dead", 43 | "not op_mini all" 44 | ], 45 | "development": [ 46 | "last 1 chrome version", 47 | "last 1 firefox version", 48 | "last 1 safari version" 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /cosmic-emoji-react/src/widgets/Preview.css: -------------------------------------------------------------------------------- 1 | .Preview { 2 | display: inline-block; 3 | height: 64px; 4 | width: 64px; 5 | border: 1px solid #ccc; 6 | margin-left: 8px; 7 | margin-right: 8px; 8 | margin-top: 10px; 9 | position: relative; 10 | border: 3px solid #333; 11 | border-radius: 5px; 12 | cursor: pointer; 13 | transition: border 0.5s ease; 14 | } 15 | 16 | /* .Preview img { 17 | position: absolute; 18 | top: 0; 19 | left: 0; 20 | height: 100px; 21 | width: 100px; 22 | } */ 23 | 24 | .Preview.preview-selected { 25 | border: 3px solid #bbbb00; 26 | } 27 | 28 | .preview-error { 29 | position: absolute; 30 | top: 20px; 31 | right: 5px; 32 | color: #aaa; 33 | } 34 | 35 | .preview-loading { 36 | position: absolute; 37 | top: 1px; 38 | right: 1px; 39 | color: #aaa; 40 | opacity: 0; 41 | transition: opacity 0.75s ease; 42 | font-size: 0.7em; 43 | } 44 | .preview-loading.preview-loading-visible { 45 | opacity: 1; 46 | } 47 | 48 | .preview-saving { 49 | position: absolute; 50 | bottom: 2px; 51 | right: 2px; 52 | color: #aaa; 53 | background-color: rgba(0, 0, 0, 0.5); 54 | border-radius: 8px; 55 | font-size: 0.85em; 56 | } 57 | 58 | .Preview .preview-name { 59 | position: absolute; 60 | top: -22px; 61 | right: -3px; 62 | left: -3px; 63 | font-size: 0.8em; 64 | border-radius: 4px; 65 | background-color: rgba(0, 0, 0, 0.8); 66 | padding: 0 4px; 67 | text-align: center; 68 | white-space: nowrap; 69 | color: #eee; 70 | transition: color 0.5s ease; 71 | } 72 | 73 | .Preview.preview-selected .preview-name { 74 | color: yellow; 75 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cosmic-unicorn 2 | 3 | A collection of projects for the Pimoroni Cosmic Unicorn 4 | https://shop.pimoroni.com/products/cosmic-unicorn 5 | 6 | These Cosmic Unicorn photos are really difficult to capture on the phone camera. In person, they are much more bright and vibrant in color. 7 | 8 | # 9 | 10 | ## [cosmic-emoji-react](cosmic-emoji-react/) 11 | 12 | ❤️ 🔥 🥰 🚀 😡 👾 🐢 ⚡️ 💸 13 | 14 | There is something very nice about having super sized emojis on display around your home or office. 15 | 16 | A project for the Pimoroni Cosmic Unicorn that allows you to paint emojis on them, and control them from a computer, phone, or tablet. 17 | - Written in TypeScript, React 18 | 19 | [cosmic-emoji-react](cosmic-emoji-react/) 20 | 21 | ![Cosmic Emojis](https://chriscarey.com/images/pimoroni/unicorn/cosmic-emoji-2.jpeg "Cosmic Emojis") 22 | 23 | # 24 | 25 | ## [cosmic paste](cosmic-paste/) 26 | 27 | A vanilla JavaScript/HTML page to paste from the clipboard to the Cosmic Unicorn 28 | 29 | Paste from clipboard to the Cosmic Unicorn (32x32 1024 pixels) 30 | Paste from clipboard to 4 Cosmic Unicorns (64x64 4096 pixels) 31 | 32 | - Written in HTML and Vanilla JavaScript 33 | 34 | [cosmic paste](cosmic-paste/) 35 | 36 | ![Cosmic Emojis](https://chriscarey.com/images/pimoroni/unicorn/cosmic-paste-1.jpeg "Cosmic Paste") 37 | 38 | # 39 | 40 | ## [cosmic doomguy](cosmic-doomguy/) 41 | 42 | Doom Guy from the video game Doom, on your Cosmic Unicorn 43 | 44 | - Written in MicroPython 45 | 46 | [cosmic doomguy](cosmic-doomguy/) 47 | 48 | ![Doomguy Image](cosmic-doomguy/screenshot/doomguy-photo-400.jpg "Doomguy Image") 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /cosmic-emoji-react/src/useQueryParamState.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | 3 | interface useQueryParamStateProps { 4 | defaultState: T; 5 | } 6 | 7 | export function useQueryParamState( 8 | name: string, 9 | type: 'string' | 'boolean' | 'number', 10 | defaultState: T, 11 | ): [T, (val: T) => void] { 12 | // TODO: If we have values on the query params, load those into default state instead of using the value passed in 13 | const searchParams = new URLSearchParams(window.location.search); 14 | let valueFromParam = searchParams.get(name); 15 | let typedValueFromParam: T = defaultState; 16 | if (valueFromParam && type === 'string') { 17 | typedValueFromParam = valueFromParam as T; 18 | } 19 | // if (valueFromParam && type === 'boolean') { 20 | // typedValueFromParam = valueFromParam === true as T; 21 | // } 22 | if (valueFromParam && type === 'number') { 23 | typedValueFromParam = parseInt(valueFromParam) as T; 24 | } 25 | const initialState = typedValueFromParam ?? defaultState; 26 | const [myState, setMyState] = useState(initialState); 27 | //document.location.search = `${name}=${myState}`; 28 | 29 | const setToState = useCallback((newValue: T) => { 30 | if (typeof newValue === 'string' || typeof newValue === 'number' || typeof newValue === 'boolean') { 31 | const searchParams = new URLSearchParams(window.location.search); 32 | searchParams.set(name, newValue.toString()); 33 | window.history.replaceState(null, '', '?' + searchParams.toString()); 34 | setMyState(newValue); 35 | } 36 | }, [setMyState]); 37 | 38 | return [myState, setToState]; 39 | }; 40 | 41 | // const useQueryParamState = (defaultState: T) => { 42 | // const [myState, setMyState] = useState(defaultState); 43 | // return [myState, setMyState]; 44 | // }; 45 | 46 | // export default useQueryParamState; 47 | -------------------------------------------------------------------------------- /cosmic-doomguy/README.md: -------------------------------------------------------------------------------- 1 | # cosmic-doomguy 2 | 3 | Doomguy, on your Pimoroni Cosmic Unicorn 4 | https://shop.pimoroni.com/products/cosmic-unicorn 5 | 6 | Press the A, B, C, or D buttons to change Doomguy's mood: 7 | - A: Happy 8 | - B: Upset 9 | - C: Angry 10 | - D: Bloody 11 | 12 | The routine will rotate through images representing each mood every 10 seconds. 13 | This value can be configured at the top of the MicroPython file. 14 | 15 | This is using PicoGraphics to load a JPEG image as a sprite that actually contains 27 images, 32x32. 16 | 17 | This script is not particularly interesting on its own, but can be used as a foundation to build upon to make Doom Guy react to things. I am currently working on another variation of this script that will react to the status of my Nagios server where the more services are in WARNING or CRITICAL state, Doom Guy gets more angry. 18 | 19 | ![Doomguy Image](screenshot/doomguy-photo-400.jpg "Doomguy Image") 20 | 21 | 22 | 23 | 24 | As with all these unicorn photos, the colors are much more vibrant in-person. These photos do not do it justice. 25 | 26 | # 27 | 28 | ## Upload the files to your pico 29 | 30 | - Upload doomguy.jpg to the root of your pico. 31 | - Upload doomguy.py to the root of your pico. 32 | 33 | "screenshot" folder is not needed for this script to run. 34 | 35 | ## Run the file 36 | 37 | - Run doomguy.py with Thonny 38 | 39 | ## Optionally make this script run when the Pico boots up 40 | 41 | If you want this script to run on boot, then you need to copy the contents of `doomguy.py` into `main.py`. main.py is the file that starts on boot. 42 | 43 | # 44 | 45 | Check out my doom-flynn-css project to see what these images look like: https://github.com/chriscareycode/doom-flynn-css 46 | 47 | Thanks to Pimoroni for creating these awesome boards! 48 | 49 | 2024 Chris Carey -------------------------------------------------------------------------------- /cosmic-emoji-react/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 30 | Cosmic Emoji 31 | 32 | 33 | 34 |
35 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /cosmic-emoji-react/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cosmic-paste/README.md: -------------------------------------------------------------------------------- 1 | # cosmic-paste 2 | 3 | A vanilla JavaScript/HTML page to paste from the clipboard to the Cosmic Unicorn 4 | 5 | Paste from clipboard to the Cosmic Unicorn (32x32 1024 pixels) 6 | Paste from clipboard to 4 Cosmic Unicorns (64x64 4096 pixels) 7 | 8 | ![Cosmic Emojis](https://chriscarey.com/images/pimoroni/unicorn/cosmic-paste-1.jpeg "Cosmic Paste") 9 | 10 | ## Install required libraries 11 | 12 | Connect your Pico to the computer and run Thonny. Select your Pico by clicking on the bottom right corner in Thonny. You should see the list of files on the Pico in the bottom left section. Stop any running process by clicking the Stop icon. 13 | 14 | In Thonny, go to Tools -> Manage Packages... 15 | 16 | Search for `microdot_asyncio` and install that. This will copy some files into the lib/ folder on your pico. It is needed for the web server that we use (same as cosmic_paint). Also search for and install `micropython-phew`. 17 | 18 | ## Setup WIFI_CONFIG.py and copy network_manager.py 19 | 20 | This script uses and requires the same `WIFI_CONFIG.py` that many of the other WiFi examples use (like cosmic_paint). That will need to be setup like this: 21 | ``` 22 | SSID="fill this in with network name" 23 | PSK="fill this in with password" 24 | COUNTRY="GB or US or your country" 25 | ``` 26 | You will also need to upload `network_manager.py` from the common folder 27 | - [micropython/examples/common](https://github.com/pimoroni/pimoroni-pico/blob/main/micropython/examples/common) 28 | 29 | https://github.com/pimoroni/pimoroni-pico/tree/main/micropython/examples/cosmic_unicorn#wireless-examples 30 | 31 | ## Installing the code on your Cosmic Unicorn Pico 32 | 33 | - From the repo, upload `cosmic-paste.py` on your Pico. 34 | - Optionally copy `cosmic-paste.py` to `main.py` if you want it to start on boot 35 | - In Thonny, run the file on the Pico, and get the IP address from the console 36 | - If it does not run, or you do not see the IP address, then setup `WIFI_CONFIG.py` or install required libraries and try again. 37 | - Copy `cosmic-paste/config-unicorns.example.json` to `cosmic-paste/config-unicorns.json` 38 | - Edit the file `cosmic-paste/config-unicorns.json` with your Unicorn IP address. 39 | - Upload `cosmic-paste/config-unicorns.json` to your Pico in a `cosmic-paste/` folder. 40 | - Upload `cosmic-paste/cosmic-paste.html` on your Pico in a `cosmic-paste/` folder. 41 | 42 | Load the user interface at `http:///` 43 | 44 | ## Local Development 45 | 46 | - `python -m SimpleHTTPServer 3070` 47 | 48 | or 49 | 50 | - `python -m http.server 3070` 51 | 52 | Open `http://localhost:3070/paste.html` in the browser 53 | 54 | [Back to top](https://github.com/chriscareycode/cosmic-unicorn/) 55 | -------------------------------------------------------------------------------- /cosmic-emoji-react/src/widgets/Brightness.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | import { debounce } from 'lodash'; 3 | import { UnicornType } from "../types/paint"; 4 | 5 | interface BrightnessProps { 6 | ip?: string; 7 | selectedIndex: number; 8 | setUnicornConfigs: React.Dispatch>; 9 | } 10 | 11 | const Brightness = ({ 12 | ip, 13 | selectedIndex, 14 | setUnicornConfigs, 15 | }: BrightnessProps) => { 16 | 17 | const [brightness, setBrightness] = useState(0.5); 18 | const [isError, setIsError] = useState(false); 19 | 20 | const getBrightness = useCallback(() => { 21 | if (!ip) return; 22 | const url = `http://${ip}/get_brightness`; 23 | const requestOptions: RequestInit = { 24 | method: 'GET', 25 | }; 26 | console.log('fetching', url); 27 | fetch(url, requestOptions) 28 | .then(response => { 29 | return response.text(); 30 | }) 31 | .then(data => { 32 | console.log('get_brightness data', data); 33 | const floatData = parseFloat(data); 34 | if (floatData >= 0 && floatData <= 10) { 35 | setBrightness(floatData); 36 | setIsError(false); 37 | } 38 | }).catch(e => { 39 | console.log('get_brightness catch'); 40 | setIsError(true); 41 | }); 42 | }, [ip]); 43 | 44 | const saveBrightness = (brightness: number) => { 45 | if (!ip) return; 46 | const payload = brightness.toString(); 47 | const url = `http://${ip}/set_brightness`; 48 | const requestOptions: RequestInit = { 49 | method: 'POST', 50 | body: payload, 51 | }; 52 | console.log('POSTing', url); 53 | fetch(url, requestOptions) 54 | .then(response => { 55 | return response.text(); 56 | }) 57 | .then(data => { 58 | console.log('set_brightness data', data); 59 | const floatData = parseFloat(data); 60 | if (floatData >= 0 && floatData <= 10) { 61 | setBrightness(floatData); 62 | setIsError(false); 63 | } 64 | }).catch(e => { 65 | console.log('set_brightness catch'); 66 | setIsError(true); 67 | }); 68 | }; 69 | 70 | const rangeChanged = debounce((e: React.ChangeEvent) => { 71 | console.log('Brightness rangeChanged', e.target.value); 72 | const tmp_brightness = parseFloat(e.target.value); 73 | setBrightness(tmp_brightness); // save to state 74 | saveBrightness(tmp_brightness); // save to pico 75 | }, 50); 76 | 77 | useEffect(() => { 78 | getBrightness(); 79 | }, [ip, getBrightness]); 80 | 81 | return ( 82 |
83 |
84 | Brightness{' '} 85 | 91 | {brightness.toFixed(1)} 92 | {isError && ERROR} 93 |
94 | 95 |
96 | ); 97 | }; 98 | 99 | export default Brightness; -------------------------------------------------------------------------------- /cosmic-emoji-react/src/widgets/Preview.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { UnicornType } from '../types/paint'; 3 | import './Preview.css'; 4 | 5 | interface PreviewProps { 6 | keyId: number; 7 | config: UnicornType; 8 | onClick: any; 9 | selected: boolean; 10 | dataRgbaArray: number[] | undefined; 11 | isLoading: boolean; 12 | isSaving: boolean; 13 | isError: boolean; 14 | } 15 | 16 | const Preview = ({ 17 | keyId, 18 | config, 19 | onClick, 20 | selected, 21 | dataRgbaArray, 22 | isLoading, 23 | isSaving, 24 | isError, 25 | }: PreviewProps) => { 26 | 27 | useEffect(() => { 28 | var c = document.getElementById(`canvas-${keyId}`) as HTMLCanvasElement; 29 | var ctx = c?.getContext("2d"); 30 | if (ctx) { 31 | ctx.scale(2, 2); 32 | } 33 | return () => { 34 | ctx?.scale(0.5, 0.5); 35 | }; 36 | }, [keyId]); 37 | 38 | /** 39 | * When dataRgbaArray changes, draw it to the Preview canvas 40 | */ 41 | useEffect(() => { 42 | var c = document.getElementById(`canvas-${keyId}`) as HTMLCanvasElement; 43 | var ctx = c?.getContext("2d"); 44 | if (ctx) { 45 | if (dataRgbaArray) { 46 | for (var y = 0; y < 32; y++) { 47 | for (var x = 0; x < 32; x++) { 48 | const index = (y * 32 + x) * 4; 49 | // get rgba data 50 | const r = dataRgbaArray[index]; 51 | const g = dataRgbaArray[index + 1]; 52 | const b = dataRgbaArray[index + 2]; 53 | const a = dataRgbaArray[index + 3]; 54 | 55 | // convert rgba to rgb 56 | const aa = a / 255; 57 | const rr = Math.round(r * aa); 58 | const gg = Math.round(g * aa); 59 | const bb = Math.round(b * aa); 60 | 61 | // Draw rgba to canvas (does not work) 62 | //ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${a})`; 63 | 64 | // Draw rgb to canvas 65 | ctx.fillStyle = `rgb(${rr}, ${gg}, ${bb})`; 66 | ctx.fillRect(x, y, 1, 1); 67 | } 68 | } 69 | } else { 70 | // no data, draw black 71 | ctx.fillStyle = `rgb(0, 0, 0)`; 72 | ctx.fillRect(0, 0, 32, 32); 73 | } 74 | } 75 | }, [keyId, dataRgbaArray]); 76 | 77 | 78 | return ( 79 |
80 | <> 81 | 93 | 94 | 95 | {/* {config.dataUrl && } */} 96 | 97 | {config.name && ( 98 |
{config.name}
99 | )} 100 | 101 | {isError && ( 102 |
😡 Error
103 | )} 104 | 105 |
♻️
106 | 107 | {isSaving && ( 108 |
💾
109 | )} 110 | 111 |
112 | ); 113 | }; 114 | 115 | export default Preview; 116 | -------------------------------------------------------------------------------- /cosmic-paste/cosmic-paste.py: -------------------------------------------------------------------------------- 1 | # 2 | # cosmic paste 3 | # 4 | # - FEATURE fix brigtness to change on the fly 5 | # - HOUSEKEEPING rename emoji_paint.py to cosmic-paste.py (and build folders, and releases) 6 | # - HOUSEKEEPING readme remove requirement network_manager.py, its not needed 7 | # - OPTIMIZE do not send rgba to pico, send rgb only (do rgba->rgb in the browser) 8 | # 9 | import os 10 | from microdot_asyncio import Microdot, Request, Response, send_file 11 | from phew import connect_to_wifi 12 | from cosmic import CosmicUnicorn 13 | from picographics import PicoGraphics, DISPLAY_COSMIC_UNICORN as DISPLAY 14 | from WIFI_CONFIG import SSID, PSK 15 | 16 | cu = CosmicUnicorn() 17 | graphics = PicoGraphics(DISPLAY) 18 | mv_graphics = memoryview(graphics) 19 | cu.set_brightness(0.5) 20 | 21 | WIDTH, HEIGHT = graphics.get_bounds() 22 | 23 | ip = connect_to_wifi(SSID, PSK) 24 | 25 | print(f"Start painting at: http://{ip}") 26 | 27 | last_pixels = "" 28 | 29 | server = Microdot() 30 | 31 | @server.route("/", methods=["GET"]) 32 | def route_index(request): 33 | return send_file("cosmic-paste/cosmic-paste.html") 34 | 35 | @server.route("/config-unicorns.json", methods=["GET"]) 36 | def route_index(request): 37 | return send_file("cosmic-paste/config-unicorns.json") 38 | 39 | # @server.route("/static/", methods=["GET"]) 40 | # def route_static(request, path): 41 | # return send_file(f"cosmic-paste/static/{path}") 42 | 43 | @server.route('/get_pixels', methods=["GET"]) 44 | def route_get_pixels(req): 45 | global last_pixels 46 | #print("get_pixels") 47 | 48 | res = Response() 49 | res.headers["Access-Control-Allow-Origin"] = '*' 50 | res.body = last_pixels 51 | return res 52 | 53 | @server.post('/set_pixels') 54 | def set_pixels(req): 55 | global last_pixels 56 | #print("got data") 57 | 58 | data = req.body 59 | 60 | # save for later 61 | last_pixels = data 62 | 63 | arr = list(data) 64 | 65 | for j in range(HEIGHT): 66 | for i in range(WIDTH): 67 | index = (j * 32 + i) * 4 68 | 69 | #convert rgba to rgb 70 | r = int(data[index]) 71 | g = int(data[index+1]) 72 | b = int(data[index+2]) 73 | a = int(data[index+3]) / 255 74 | 75 | r = round(a * r) 76 | g = round(a * g) 77 | b = round(a * b) 78 | 79 | # set the pixel 80 | graphics.set_pen(graphics.create_pen(r, g, b)) 81 | graphics.pixel(i, j) 82 | cu.update(graphics) 83 | 84 | res = Response() 85 | res.headers["Access-Control-Allow-Origin"] = '*' 86 | res.body = "success" 87 | return res 88 | 89 | @server.route("/get_brightness", methods=["GET"]) 90 | def get_brightness(req): 91 | res = Response() 92 | res.headers["Access-Control-Allow-Origin"] = '*' 93 | res.body = str(cu.get_brightness()) 94 | return res 95 | 96 | @server.post("/set_brightness") 97 | def set_brightness(req): 98 | res = Response() 99 | res.headers["Access-Control-Allow-Origin"] = '*' 100 | res.body = "failure" 101 | 102 | brightness = req.body 103 | if brightness: 104 | brightnessFloat = float(brightness) 105 | if brightnessFloat >= 0 and brightnessFloat <= 10: 106 | cu.set_brightness(brightnessFloat) 107 | res.body = "success" 108 | return res 109 | 110 | # start the web server on all ips, port 80 111 | server.run(host="0.0.0.0", port=80) 112 | -------------------------------------------------------------------------------- /cosmic-emoji-react/pico/cosmic-emoji.py: -------------------------------------------------------------------------------- 1 | # 2 | # cosmic emoji 3 | # 4 | # - FEATURE fix brigtness to change on the fly 5 | # - HOUSEKEEPING rename emoji_paint.py to cosmic-emoji.py (and build folders, and releases) 6 | # - HOUSEKEEPING readme remove requirement network_manager.py, its not needed 7 | # - OPTIMIZE do not send rgba to pico, send rgb only (do rgba->rgb in the browser) 8 | # 9 | # 10 | import os 11 | from microdot_asyncio import Microdot, Request, Response, send_file 12 | from phew import connect_to_wifi 13 | from cosmic import CosmicUnicorn 14 | from picographics import PicoGraphics, DISPLAY_COSMIC_UNICORN as DISPLAY 15 | from WIFI_CONFIG import SSID, PSK 16 | 17 | cu = CosmicUnicorn() 18 | graphics = PicoGraphics(DISPLAY) 19 | mv_graphics = memoryview(graphics) 20 | cu.set_brightness(0.5) 21 | 22 | WIDTH, HEIGHT = graphics.get_bounds() 23 | 24 | ip = connect_to_wifi(SSID, PSK) 25 | 26 | print(f"Connected to wifi [{SSID}] ip [{ip}]") 27 | 28 | last_pixels = "" 29 | 30 | # start web server 31 | server = Microdot() 32 | 33 | print(f"Start painting at: http://{ip}") 34 | 35 | @server.route("/", methods=["GET"]) 36 | def route_index(request): 37 | return send_file("cosmic-emoji/index.html") 38 | 39 | @server.route("/config-unicorns.json", methods=["GET"]) 40 | def route_index(request): 41 | return send_file("cosmic-emoji/config-unicorns.json") 42 | 43 | @server.route("/static/", methods=["GET"]) 44 | def route_static(request, path): 45 | return send_file(f"cosmic-emoji/static/{path}") 46 | 47 | @server.route('/get_pixels', methods=["GET"]) 48 | def route_get_pixels(req): 49 | global last_pixels 50 | #print("get_pixels") 51 | 52 | res = Response() 53 | res.headers["Access-Control-Allow-Origin"] = '*' 54 | res.body = last_pixels 55 | return res 56 | 57 | def draw_pixels(): 58 | global last_pixels 59 | data = last_pixels 60 | arr = list(data) 61 | 62 | for j in range(HEIGHT): 63 | for i in range(WIDTH): 64 | index = (j * 32 + i) * 4 65 | 66 | #convert rgba to rgb 67 | r = int(data[index]) 68 | g = int(data[index+1]) 69 | b = int(data[index+2]) 70 | a = int(data[index+3]) / 255 71 | 72 | r = round(a * r) 73 | g = round(a * g) 74 | b = round(a * b) 75 | 76 | # set the pixel 77 | graphics.set_pen(graphics.create_pen(r, g, b)) 78 | graphics.pixel(i, j) 79 | cu.update(graphics) 80 | 81 | @server.post('/set_pixels') 82 | def set_pixels(req): 83 | global last_pixels 84 | 85 | # save for later 86 | last_pixels = req.body 87 | 88 | draw_pixels() 89 | 90 | res = Response() 91 | res.headers["Access-Control-Allow-Origin"] = '*' 92 | res.body = "success" 93 | return res 94 | 95 | @server.route("/get_brightness", methods=["GET"]) 96 | def get_brightness(req): 97 | res = Response() 98 | res.headers["Access-Control-Allow-Origin"] = '*' 99 | res.body = str(cu.get_brightness()) 100 | return res 101 | 102 | @server.post("/set_brightness") 103 | def set_brightness(req): 104 | res = Response() 105 | res.headers["Access-Control-Allow-Origin"] = '*' 106 | res.body = "failure" 107 | 108 | brightness = req.body 109 | if brightness: 110 | brightnessFloat = float(brightness) 111 | if brightnessFloat >= 0 and brightnessFloat <= 10: 112 | cu.set_brightness(brightnessFloat) 113 | res.body = "success" 114 | # after changing brightness you have to draw the pixels again 115 | # to see the brightness change 116 | draw_pixels() 117 | return res 118 | 119 | # start the web server on all ips, port 80 120 | server.run(host="0.0.0.0", port=80) 121 | -------------------------------------------------------------------------------- /cosmic-doomguy/doomguy.py: -------------------------------------------------------------------------------- 1 | # doomguy.py 2 | # 3 | # Upload doomguy.jpg and doom-guy.py to your Pico with Thonny. 4 | # Then run doomguy.py. 5 | # 6 | # Optionally copy doom-guy.py to main.py to run on startup 7 | 8 | import time 9 | from cosmic import CosmicUnicorn 10 | from picographics import PicoGraphics, DISPLAY_COSMIC_UNICORN as DISPLAY 11 | import jpegdec 12 | import random 13 | 14 | ######################################################################## 15 | # edit this configuration 16 | ######################################################################## 17 | # 18 | update_image_every_how_many_seconds = 5 19 | # 20 | ######################################################################## 21 | 22 | # create cosmic object and graphics surface for drawing 23 | cu = CosmicUnicorn() 24 | graphics = PicoGraphics(DISPLAY) 25 | 26 | width = CosmicUnicorn.WIDTH 27 | height = CosmicUnicorn.HEIGHT 28 | 29 | # clear the background 30 | graphics.rectangle(0, 0, width, height) 31 | 32 | # set the brightness 33 | cu.set_brightness(0.6) 34 | 35 | ######################################################################## 36 | # open the jpeg file 37 | ######################################################################## 38 | 39 | print("trying to open doomguy.jpg file...") 40 | jpeg = jpegdec.JPEG(graphics) 41 | jpeg.open_file("doomguy.jpg") 42 | print("opened doomguy.jpg file.") 43 | 44 | ######################################################################## 45 | # doom guy constants and variables 46 | ######################################################################## 47 | 48 | # configure moods that map to sprite numbers 49 | happy_images = [1, 6, 11] 50 | upset_images = [2, 3, 7, 8, 12, 16, 17] 51 | angry_images = [4, 9, 13, 18, 19] 52 | bloody_images = [5, 10, 14, 15, 20, 25, 26] 53 | 54 | doomguys_mood = "happy" 55 | doomguys_prev_mood = "happy" 56 | 57 | ######################################################################## 58 | # doom guy functions 59 | ######################################################################## 60 | 61 | def show_doomguy_by_mood(mood): 62 | my_random = 1 63 | if mood == "happy": 64 | my_random = random.choice(happy_images) 65 | elif mood == "upset": 66 | my_random = random.choice(upset_images) 67 | elif mood == "angry": 68 | my_random = random.choice(angry_images) 69 | elif mood == "bloody": 70 | my_random = random.choice(bloody_images) 71 | show_doomguy_by_number(my_random) 72 | print("show doom guy mood:", mood, my_random) 73 | 74 | # takes a number from 1 to 27 75 | def show_doomguy_by_number(num): 76 | if num < 1: 77 | print("out of bounds. changing to 1") 78 | num = 1; 79 | if num > 27: 80 | print("out of bounds. changing to 27") 81 | num = 27; 82 | y_offset = -1 * (num - 1) * 32; 83 | # write the jpeg to screen 84 | jpeg.decode(4, y_offset, jpegdec.JPEG_SCALE_HALF) 85 | # update the pixels 86 | cu.update(graphics) 87 | 88 | ######################################################################## 89 | # run a demo on start 90 | ######################################################################## 91 | 92 | print("program starting... Push A, B, C or D to change his mood") 93 | print("demo some sprites...") 94 | 95 | show_doomguy_by_number(random.choice(happy_images)) 96 | time.sleep(0.5) 97 | show_doomguy_by_number(random.choice(upset_images)) 98 | time.sleep(0.5) 99 | show_doomguy_by_number(random.choice(angry_images)) 100 | time.sleep(0.5) 101 | show_doomguy_by_number(random.choice(bloody_images)) 102 | time.sleep(0.5) 103 | show_doomguy_by_number(27) 104 | time.sleep(1.5) 105 | show_doomguy_by_number(random.choice(happy_images)) 106 | 107 | ######################################################################## 108 | # timer function to run routines on an interval 109 | ######################################################################## 110 | 111 | def fast_timer_function(timer): 112 | global doomguys_mood 113 | show_doomguy_by_mood(doomguys_mood) 114 | 115 | 116 | # timer to animate doom guy in the current mood 117 | timer_fast = machine.Timer(-1) 118 | timer_fast_period_ms = update_image_every_how_many_seconds * 1000 119 | timer_fast.init(period=timer_fast_period_ms, mode=machine.Timer.PERIODIC, callback=fast_timer_function) 120 | 121 | ######################################################################## 122 | # Keep the program running 123 | ######################################################################## 124 | 125 | try: 126 | while True: 127 | # Handle the button press events 128 | if cu.is_pressed(CosmicUnicorn.SWITCH_BRIGHTNESS_UP): 129 | cu.adjust_brightness(+0.1) 130 | cu.update(graphics) 131 | print("adusted brightness up", cu.get_brightness()) 132 | 133 | if cu.is_pressed(CosmicUnicorn.SWITCH_BRIGHTNESS_DOWN): 134 | cu.adjust_brightness(-0.1) 135 | cu.update(graphics) 136 | print("adusted brightness down", cu.get_brightness()) 137 | 138 | if cu.is_pressed(CosmicUnicorn.SWITCH_A): 139 | doomguys_mood = "happy" 140 | print("setting mood to", doomguys_mood) 141 | show_doomguy_by_mood(doomguys_mood) 142 | 143 | if cu.is_pressed(CosmicUnicorn.SWITCH_B): 144 | doomguys_mood = "upset" 145 | print("setting mood to", doomguys_mood) 146 | show_doomguy_by_mood(doomguys_mood) 147 | 148 | if cu.is_pressed(CosmicUnicorn.SWITCH_C): 149 | doomguys_mood = "angry" 150 | print("setting mood to", doomguys_mood) 151 | show_doomguy_by_mood(doomguys_mood) 152 | 153 | if cu.is_pressed(CosmicUnicorn.SWITCH_D): 154 | doomguys_mood = "bloody" 155 | print("setting mood to", doomguys_mood) 156 | show_doomguy_by_mood(doomguys_mood) 157 | 158 | time.sleep(0.1) 159 | pass 160 | except KeyboardInterrupt: 161 | # Cancel the timer and clean up before exiting 162 | timer_fast.deinit() 163 | print("timer_fast canceled") 164 | -------------------------------------------------------------------------------- /cosmic-emoji-react/README.md: -------------------------------------------------------------------------------- 1 | # cosmic-emoji-react 2 | 3 | ❤️ 🔥 🥰 🚀 😡 👾 🐢 ⚡️ 💸 4 | 5 | There is something very nice about having super sized emojis on display around your home or office. 6 | 7 | A project for the Pimoroni Cosmic Unicorn that allows you to paint emojis on them, and control them from a computer, phone, or tablet. https://shop.pimoroni.com/products/cosmic-unicorn 8 | 9 | * Also supports pasting any image from your clipboard to the Cosmic Unicorn 10 | 11 | ![Cosmic Emojis](https://chriscarey.com/images/pimoroni/unicorn/cosmic-emoji-2.jpeg "Cosmic Emojis") 12 | 13 | Interface on mobile: 14 | 15 | Mobile 16 | 17 | # 18 | 19 | ## Install required libraries 20 | 21 | Connect your Pico to the computer and run Thonny. Select your Pico by clicking on the bottom right corner in Thonny. You should see the list of files on the Pico in the bottom left section. Stop any running process by clicking the Stop icon. 22 | 23 | In Thonny, go to Tools -> Manage Packages... 24 | 25 | Search for `microdot_asyncio` and install that. This will copy some files into the lib/ folder on your pico. It is needed for the web server that we use (same as cosmic_paint). Also search for and install `micropython-phew`. 26 | 27 | ## Setup WIFI_CONFIG.py and copy network_manager.py 28 | 29 | This script uses and requires the same `WIFI_CONFIG.py` that many of the other WiFi examples use (like cosmic_paint). That will need to be setup like this: 30 | ``` 31 | SSID="fill this in with network name" 32 | PSK="fill this in with password" 33 | COUNTRY="GB or US or your country" 34 | ``` 35 | 36 | You will also need to upload `network_manager.py` from the common folder 37 | - [micropython/examples/common](https://github.com/pimoroni/pimoroni-pico/blob/main/micropython/examples/common) 38 | 39 | https://github.com/pimoroni/pimoroni-pico/tree/main/micropython/examples/cosmic_unicorn#wireless-examples 40 | 41 | ## Installing on multiple Cosmic Unicorns 42 | 43 | A note that if you are installing this on multiple Cosmic Unicorns, that you can install the user interface code (which is quite large) on only one device. On the other devices you only need to install the server code `cosmic-emoji.py` 44 | 45 | ## Installing the server code without user interface 46 | 47 | - From the repo, install `cosmic-emoji.py` on your Pico. 48 | - Optionally copy `cosmic-emoji.py` to `main.py` if you want it to start on boot 49 | - In Thonny, run the file on the Pico, and get the IP address from the console 50 | - If it does not run, or you do not see the IP address, then setup `WIFI_CONFIG.py` or install required libraries and try again. 51 | 52 | If you go this route, then you will need to run the user interface on your computer or somewhere else. You will need the IP address above for configuration in the user interface. 53 | 54 | If you want to install both the server code and user interface on the Pico, read on.. 55 | 56 | ## Installing the server code and user interface 57 | 58 | Here are the instructions for running the server code and user interface on the Pico, using the latest pre-built release: 59 | 60 | - Download the latest from the GitHub releases. [https://github.com/chriscareycode/cosmic-unicorn/releases](https://github.com/chriscareycode/cosmic-unicorn/releases) . You only need the cosmic-emoji-react-v0.0.4.zip file, not the source code files. 61 | 62 | - Extract the file. 63 | 64 | - Copy `cosmic-emoji/config-unicorns.example.json` to `cosmic-emoji/config-unicorns.json` 65 | 66 | - Edit the file `cosmic-emoji/config-unicorns.json` with your Unicorn IP address. 67 | 68 | - If you have multiple Cosmic Unicorns, you can add multiple entries to control them all. Make sure to add commas in the right spots so the file is valid JSON format. You may want to install the user interface only on one of the Picos, and install server files only on the others. It's up to you. 69 | 70 | ### Edit the config file with your Pico IP address 71 | 72 | If you know the IP address that your pico has, then edit that IP address in the `config-unicorns.json` file. 73 | 74 | If you do not know the IP address, then upload `cosmic-emoji.py` and run that. In the Thonny console it will tell your the IP address if WiFi connection is successful. Edit the `config-unicorns.json` with the IP address of your pico and save the config file. 75 | 76 | ### Upload the files (with user interface) 77 | 78 | Upload the rest of the files to the pico using Thonny or another tool. 79 | 80 | Copy the files to your pico: 81 | - cosmic-emoji.py 82 | - cosmic-emoji/index.html 83 | - cosmic-emoji/config-unicorns.json 84 | - cosmic-emoji/static/ (folder with css and js files) 85 | 86 | `cosmic-emoji.py` is the server and goes in the root of your pico, and the `cosmic-emoji/` folder contains the user interface. 87 | 88 | ### Optionally make this script run when the Pico boots up 89 | 90 | If you want this script to run on boot, then you need to copy the contents of `cosmic-emoji.py` into `main.py`. main.py is the file that starts on boot. 91 | 92 | 93 | 94 | 95 | You're all set. Start the program and look for the IP address to connect to. 96 | 97 | # 98 | # Running the user interface in Development mode 99 | 100 | 101 | ### Running in local development 102 | 103 | This project started as a standard React app created with create-react-app. Then TypeScript was added. Then switched to Preact (to reduce the bundle size). The bundle is still quite large (in pico terms) at like 290KB. 104 | 105 | Git Clone this project 106 | 107 | - `git clone https://github.com/chriscareycode/cosmic-unicorn.git` 108 | 109 | Change into cosmic-emoji-react folder 110 | 111 | - `cd cosmic-emoji-react` 112 | 113 | Setup the config-unicorns.json file: 114 | 115 | - Copy `public/config-unicorns.example.json` to `public/config-unicorns.json` 116 | 117 | - Edit the file `public/config-unicorns.json` with your Unicorn IP address. 118 | 119 | You need Node.js/npm installed to do development on this project. 120 | 121 | ### `npm install` 122 | ### `npm start` 123 | 124 | Then connect to the local IP address and do your local development! Open [http://localhost:3069](http://localhost:3069) to view it in the browser. 125 | 126 | ### Building the project for use on the Pico 127 | 128 | When you are done editing files, create a build with this command: 129 | 130 | ### `npm run build` 131 | 132 | Then run this special delete command to delete the extra files in the build folder to free up needed space (since the pico has such little space): 133 | 134 | ### `./delete-extra-build-files.sh` 135 | 136 | The built files will be in the pico/ folder. 137 | 138 | Upload the files to your pico with Thonny. 139 | 140 | # 141 | 142 | Thanks to Pimoroni for creating these awesome boards! 143 | 144 | 2023 Chris Carey -------------------------------------------------------------------------------- /cosmic-paste/cosmic-paste.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | Paste to Cosmic Unicorn 14 | 66 | 67 | 68 | 69 |

Paste to Cosmic Unicorn

70 |
71 | Control-V or Command-V to paste your image. 72 |
73 | The image must be perfectly square or it will be distorted 74 |
75 |
76 |
77 | 78 |
79 | Paste to: 80 | 81 | 82 | 86 |
87 | 89 |
90 | 91 |
92 | 93 |
94 | DEBUG AREA:
95 | <img> tag: 96 | canvas: 97 |
98 | 99 | 412 | 413 | 414 | -------------------------------------------------------------------------------- /cosmic-emoji-react/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useState } from 'react'; 2 | import EmojiPicker, { Emoji, EmojiClickData, EmojiStyle, Theme } from 'emoji-picker-react'; 3 | import { useIdleTimer } from 'react-idle-timer' 4 | 5 | import Preview from './widgets/Preview'; 6 | import Brightness from './widgets/Brightness'; 7 | 8 | //import Settings from './widgets/Settings'; // might use later 9 | 10 | import { UnicornType } from './types/paint'; 11 | import { useQueryParamState } from './useQueryParamState'; 12 | import './App.css'; 13 | import './widgets/Controls.css'; 14 | 15 | interface FetchStateObject { 16 | isSaving: boolean; 17 | isLoading: boolean; 18 | isError: boolean; 19 | errorMessage: string; 20 | errorCount: number; 21 | }; 22 | 23 | interface FetchStateType { 24 | [key: string]: FetchStateObject; 25 | }; 26 | 27 | function App() { 28 | 29 | const [isConfigError, setIsConfigError] = useState(false); 30 | const [configErrorMessage, setConfigErrorMessage] = useState(''); 31 | const [selectedIndex, setSelectedIndex] = useQueryParamState('selected', 'number', 0); 32 | const [isIdle, setIsIdle] = useState(false); 33 | const [fetchState, setFetchState] = useState({}); 34 | const [unicornConfigs, setUnicornConfigs] = useState([]); 35 | const [unifiedCode, setUnifiedCode] = useState('1f423'); 36 | // const [imageLoadedAt, setImageLoadedAt] = useState(0); 37 | const [emojiStyle, setEmojiStyle] = useQueryParamState('style', 'string', EmojiStyle.APPLE); 38 | 39 | /** 40 | * Load config file 41 | */ 42 | useEffect(() => { 43 | const configFile = 'config-unicorns.json'; 44 | const configExampleFile = 'config-unicorns.example.json'; 45 | console.log(`Reading config file ${configFile}...`); 46 | fetch(configFile) 47 | .then(response => response.json()) 48 | .then(json => { 49 | console.log(json); 50 | setUnicornConfigs(json); 51 | setIsConfigError(false); 52 | // If we have multiple Cosmic Unicorns in the config, select which one to start with. 53 | // If we are hosting this UI on one of the unicorns, auto-select that one. 54 | if (json.length > 1) { 55 | for (let i = 0; i < json.length; i++) { 56 | if (json[i].ip === document.location.host) { 57 | setSelectedIndex(i); 58 | } 59 | } 60 | } 61 | }).catch(e => { 62 | setIsConfigError(true); 63 | setConfigErrorMessage(`ERROR: Was not able to load the config file ${configFile}. Make sure it exists. Copy example from ${configExampleFile}.`); 64 | }); 65 | }, [ 66 | setUnicornConfigs, 67 | setSelectedIndex, 68 | setIsConfigError, 69 | setConfigErrorMessage, 70 | ]); 71 | 72 | /* Stop polling for image updates if the page is inactive */ 73 | const onIdle = () => { 74 | console.log('onIdle'); 75 | setIsIdle(true); 76 | } 77 | 78 | /* Start polling for image updates if the page is active */ 79 | const onActive = () => { 80 | console.log('onActive'); 81 | setIsIdle(false); 82 | } 83 | 84 | /* Idle timer used for onIdle and onActive */ 85 | /* eslint-disable @typescript-eslint/no-unused-vars */ 86 | const { getRemainingTime } = useIdleTimer({ 87 | onIdle, 88 | onActive, 89 | timeout: 5 * 60 * 1000, // 5 minutes 90 | throttle: 500 91 | }); 92 | /* eslint-enable @typescript-eslint/no-unused-vars */ 93 | 94 | const sendPixelsToUnicorn = useCallback(async (payload: any) => { 95 | const config = unicornConfigs[selectedIndex]; 96 | if (!config) { 97 | console.log('Cant find config', unicornConfigs, selectedIndex); 98 | return; 99 | } 100 | const ip = config.ip; 101 | setFetchState(curr => { 102 | return { 103 | ...curr, 104 | [ip]: { 105 | ...curr[ip], 106 | isSaving: true, 107 | } 108 | }; 109 | }); 110 | 111 | const url = `http://${ip}/set_pixels`; 112 | 113 | const requestOptions: RequestInit = { 114 | method: 'POST', 115 | body: payload 116 | }; 117 | fetch(url, requestOptions) 118 | .then(response => response.text()) 119 | .then(data => { 120 | if (data === 'success') { 121 | // dataUrl is used for showing the preview 122 | //unicornConfigs[selectedIndex].dataUrl = dataUrl; 123 | unicornConfigs[selectedIndex].dataRgbaArray = payload; 124 | } 125 | }).finally(() => { 126 | setFetchState(curr => { 127 | return { 128 | ...curr, 129 | [ip]: { 130 | ...curr[ip], 131 | isSaving: false, 132 | } 133 | }; 134 | }); 135 | }); 136 | }, [selectedIndex, unicornConfigs]); 137 | 138 | const getImageDataFromEmojiWithCanvas = useCallback(() => { 139 | const img = document.querySelector('.canvas-area img') as any; 140 | //console.log('img', img); 141 | if (img) { 142 | img.setAttribute('crossOrigin', 'Anonymous'); 143 | const c = document.querySelector('#canv') as HTMLCanvasElement; 144 | //const ctx = c.getContext('2d', { willReadFrequently: true }); 145 | const ctx = c.getContext('2d'); 146 | if (ctx) { 147 | ctx.clearRect(0, 0, 32, 32); 148 | console.log('Writing img to canvas...'); 149 | ctx.drawImage(img, 0, 0, 32, 32); 150 | var d = ctx.getImageData(0, 0, 32, 32); 151 | console.log('Canvas getImageData() result:', d); 152 | return d; 153 | } 154 | } else { 155 | return null; 156 | } 157 | }, []); 158 | 159 | /** 160 | * useEffect hook to attach the paste event catcher 161 | */ 162 | useEffect(() => { 163 | 164 | const onPaste = (event: any) => { 165 | const items = event.clipboardData?.items; 166 | if (!items) { return; } 167 | for (let i = 0; i < items.length; i++) { 168 | const item = items[i]; 169 | 170 | if (item.type.indexOf("image") !== -1) { 171 | const blob = item.getAsFile(); 172 | const reader = new FileReader(); 173 | 174 | reader.addEventListener("load", function() { 175 | console.log('reader.result is', reader.result); 176 | //imagePreview.src = reader.result; 177 | const img = document.querySelector('.canvas-area img') as any; 178 | img.src = reader.result; 179 | setTimeout(() => { 180 | const d = getImageDataFromEmojiWithCanvas(); 181 | if (d) { 182 | sendPixelsToUnicorn(d.data); 183 | } 184 | }, 0); 185 | }); 186 | console.log('blob', blob); 187 | if (blob) { 188 | const slicedBlob = blob.slice(0, blob.size, blob.type); 189 | console.log('slicedBlob', slicedBlob); 190 | reader.readAsDataURL(slicedBlob); 191 | } 192 | } 193 | } 194 | }; 195 | 196 | document.addEventListener("paste", onPaste); 197 | return () => { 198 | document.removeEventListener("paste", onPaste); 199 | }; 200 | }, [ 201 | unicornConfigs, 202 | selectedIndex, 203 | getImageDataFromEmojiWithCanvas, 204 | sendPixelsToUnicorn, 205 | ]); 206 | 207 | 208 | useEffect(() => { 209 | setTimeout(() => { 210 | getImageDataFromEmojiWithCanvas(); 211 | }, 1000); 212 | }, [getImageDataFromEmojiWithCanvas]); 213 | 214 | const onEmojiClick = (e: EmojiClickData) => { 215 | // Set unified code into state 216 | // This will trigger the component to draw (async) 217 | setUnifiedCode(e.unified); 218 | 219 | // Detect when the component loads 220 | // So we know when we can pull the image data off it 221 | const img = document.querySelector('.canvas-area img') as any; 222 | console.log('got emoji img', img); 223 | if (img) { 224 | img.onload = () => { 225 | console.log('emoji img loaded', img); 226 | const d = getImageDataFromEmojiWithCanvas(); 227 | if (d) { 228 | sendPixelsToUnicorn(d.data); 229 | } 230 | //setImageLoadedAt(Date.now()); 231 | img.onload = null; 232 | }; 233 | } 234 | }; 235 | 236 | const getPixelsFromUnicorn = useCallback((index: number) => { 237 | // Get the IP from the unicorn config 238 | const ip = unicornConfigs[index].ip; 239 | 240 | // Set loading state for this unicorn 241 | setFetchState(curr => { 242 | return { 243 | ...curr, 244 | [ip]: { 245 | ...curr[ip], 246 | isLoading: true, 247 | } 248 | }; 249 | }); 250 | 251 | const url = `http://${ip}/get_pixels`; 252 | const requestOptions: RequestInit = { 253 | method: 'GET', 254 | }; 255 | fetch(url, requestOptions) 256 | .then(response => { 257 | //console.log('got response', response.body.length); 258 | //return response.blob(); 259 | return response.arrayBuffer(); 260 | }) 261 | .then(data => { 262 | const arrayFromBuffer = new Uint8Array(data); 263 | // Convert Uint8Array into number[] 264 | const numberArray = []; 265 | for (var i = 0; i < arrayFromBuffer.length - 1; i++) { 266 | numberArray.push(arrayFromBuffer[i]); 267 | } 268 | 269 | unicornConfigs[index].dataRgbaArray = numberArray; 270 | 271 | setFetchState(curr => { 272 | return { 273 | ...curr, 274 | [ip]: { 275 | ...curr[ip], 276 | isLoading: false, 277 | isError: false, 278 | errorMessage: '', 279 | errorCount: 0, 280 | } 281 | }; 282 | }); 283 | 284 | }).catch((err) => { 285 | console.log('ERROR: get_pixels catch'); 286 | unicornConfigs[index].dataRgbaArray = undefined; 287 | 288 | setFetchState(curr => { 289 | return { 290 | ...curr, 291 | [ip]: { 292 | ...curr[ip], 293 | isLoading: false, 294 | isError: true, 295 | errorMessage: `Error loading ${url}`, 296 | errorCount: curr[url] ? curr[url].errorCount + 1 : 1, 297 | } 298 | }; 299 | }); 300 | }); 301 | }, [unicornConfigs, setFetchState]); 302 | 303 | /** 304 | * This useEffect is used to update the Previews every so often 305 | */ 306 | useEffect(() => { 307 | 308 | const updateEveryHowManySeconds = 30; 309 | 310 | for (let i = 0; i < unicornConfigs.length; i++) { 311 | getPixelsFromUnicorn(i); 312 | } 313 | 314 | let interv: NodeJS.Timer | undefined; 315 | if (isIdle === false) { 316 | interv = setInterval(() => { 317 | for (let i = 0; i < unicornConfigs.length; i++) { 318 | getPixelsFromUnicorn(i); 319 | } 320 | }, updateEveryHowManySeconds * 1000); 321 | } 322 | return () => { 323 | if (interv) { 324 | clearInterval(interv); 325 | } 326 | }; 327 | }, [isIdle, getPixelsFromUnicorn, unicornConfigs.length]); 328 | 329 | /* The array of previews that we display at the top of the page */ 330 | const previewLoop = unicornConfigs.map((u, i) => { 331 | const ip = unicornConfigs[i].ip; 332 | return ( 333 | { 343 | setSelectedIndex(i); 344 | }} 345 | /> 346 | ); 347 | }); 348 | 349 | /* The array of connection errors that we show under the previews */ 350 | const errorLoop = Object.keys(fetchState).map((ip, i) => { 351 | if (fetchState[ip].isError) { 352 | return
{fetchState[ip].errorMessage} x{fetchState[ip].errorCount}
; 353 | } else { 354 | return undefined; 355 | } 356 | }); 357 | 358 | const onChangeStyle = (e: string) => { 359 | if (e === EmojiStyle.APPLE) { 360 | setEmojiStyle(EmojiStyle.APPLE); 361 | } 362 | if (e === EmojiStyle.GOOGLE) { 363 | setEmojiStyle(EmojiStyle.GOOGLE); 364 | } 365 | if (e === EmojiStyle.FACEBOOK) { 366 | setEmojiStyle(EmojiStyle.FACEBOOK); 367 | } 368 | if (e === EmojiStyle.TWITTER) { 369 | setEmojiStyle(EmojiStyle.TWITTER); 370 | } 371 | }; 372 | 373 | /** 374 | * Keypress handler to quickly select a Cosmic from multiple 375 | */ 376 | useEffect(() => { 377 | function handleKeyPress(event: KeyboardEvent) { 378 | console.log('Keyboard was pressed', event.key, typeof event.key); 379 | const parsed = parseInt(event.key); 380 | // console.log('Keyboard parsed to int', parsed); 381 | if (!isNaN(parsed)) { 382 | // console.log('unicornConfigs.length', unicornConfigs.length); 383 | if (unicornConfigs.length >= parsed) { 384 | setSelectedIndex(parsed - 1); // 0 based 385 | } 386 | } 387 | } 388 | window.addEventListener('keydown', handleKeyPress); 389 | return () => { 390 | window.removeEventListener('keydown', handleKeyPress); 391 | }; 392 | }, [unicornConfigs, setSelectedIndex]); 393 | 394 | return ( 395 |
396 | 397 |
398 | 399 | {/* Show previews of the Cosmic Unicorns */} 400 |
401 | {previewLoop} 402 |
403 | 404 | {/* Show config error message if the file is missing */} 405 | {isConfigError && ( 406 |
{configErrorMessage}
407 | )} 408 | 409 | {/* Show connection errors to individual unicorns */} 410 | {errorLoop} 411 | 412 | 417 | 418 |
419 |
420 | Emoji Style:{' '}  421 | 427 |
428 |
429 | 430 |
431 | 438 |
439 | 440 | {/** 441 | * This area is used internally by the program to perform the emoji magic. 442 | * First we write the picture to an tag, 443 | * (here we are using the component that will render us an img tag) 444 | * Then we copy that image onto canvas to get the byte array 445 | */} 446 |
447 | IMG: 453 | Canvas: 454 |
455 | 456 | {/* Settings area that we might use later */} 457 | {/* */} 461 | 462 |
463 | ); 464 | } 465 | 466 | export default App; 467 | --------------------------------------------------------------------------------